diff --git a/asgi_app.py b/asgi_app.py
index ecbb113..f1f91bc 100755
--- a/asgi_app.py
+++ b/asgi_app.py
@@ -58,21 +58,19 @@ except ImportError:
auth_enabled = os.getenv("ENABLE_AUTH", "false").lower() == "true"
bearer_auth = None
-if auth_enabled and CLERK_SECRET_KEY and CLERK_ISSUER:
+if CLERK_SECRET_KEY and CLERK_ISSUER:
# Production: Use Clerk JWKS endpoint for token validation
- # JWT token shows issuer as "https://clerk.yargimcp.com", so use that for JWKS
- jwt_issuer = "https://clerk.yargimcp.com"
bearer_auth = BearerAuthProvider(
- jwks_uri=f"{jwt_issuer}/.well-known/jwks.json",
- issuer=jwt_issuer, # Enable issuer validation with correct issuer
+ jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
+ issuer=None, # Disable issuer validation - Clerk uses different issuer format
algorithm="RS256",
- audience=None, # Disable audience validation - Clerk tokens vary
- required_scopes=[] # Disable scope validation - rely on token presence
+ audience=None, # Disable audience validation - Clerk uses different audience format
+ required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
)
- logger.info(f"Bearer auth configured with Clerk JWKS: {jwt_issuer}/.well-known/jwks.json (issuer validation enabled)")
-elif auth_enabled:
- # Development: Generate RSA key pair for testing when auth is enabled but no Clerk
- logger.warning("Authentication enabled but no Clerk credentials - using development RSA key pair")
+ 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,
@@ -89,13 +87,10 @@ elif auth_enabled:
scopes=["yargi.read", "yargi.search"],
expires_in_seconds=3600 * 24 # 24 hours for development
)
- logger.debug("Development Bearer token generated (masked for security)") # Don't log actual token
-else:
- # Authentication disabled - allow unauthenticated access
- logger.info("Authentication disabled - MCP server will allow unauthenticated access")
+ logger.info(f"Development Bearer token: {dev_token}")
# Create MCP app with Bearer authentication
-mcp_server = create_app(auth=bearer_auth)
+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="/")
@@ -126,41 +121,13 @@ class UTF8JSONResponse(JSONResponse):
separators=(",", ":"),
).encode("utf-8")
-# CORS middleware configuration - Allow Claude AI and Clerk domains
-cors_allowed_origins = ["*"]
-
custom_middleware = [
Middleware(
CORSMiddleware,
- allow_origins=cors_allowed_origins,
- allow_credentials=True, # Enable credentials for cross-origin requests
- allow_methods=["GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "PATCH"],
- allow_headers=[
- "Content-Type",
- "Authorization",
- "X-Request-ID",
- "X-Session-ID",
- "MCP-Protocol-Version",
- "Mcp-Session-Id",
- "x-api-key", # Added from your config
- "Last-Event-ID", # Added from your config for SSE support
- "Accept",
- "Origin",
- "User-Agent",
- "DNT",
- "Cache-Control",
- "X-Mx-ReqToken",
- "Keep-Alive",
- "X-Requested-With",
- "If-Modified-Since"
- ],
- expose_headers=[
- "Content-Type", # Added from your config
- "Authorization",
- "x-api-key", # Added from your config
- "Mcp-Session-Id"
- ],
- max_age=86400, # Added from your config (24 hours)
+ allow_origins=cors_origins,
+ allow_credentials=True,
+ allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
+ allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
),
]
@@ -637,7 +604,7 @@ async def mcp_token_endpoint(request: Request):
content={"error": "invalid_request", "error_description": e.detail}
)
-# Mount MCP app at /mcp/ with trailing slash (v0.1.6 approach)
+# Mount MCP app at /mcp/ with trailing slash
app.mount("/mcp/", mcp_app)
# Set the lifespan context after mounting
diff --git a/mcp_auth_http_simple.py b/mcp_auth_http_simple.py
index fd6d2d8..2b6bbfd 100644
--- a/mcp_auth_http_simple.py
+++ b/mcp_auth_http_simple.py
@@ -72,24 +72,14 @@ async def get_oauth_metadata():
return JSONResponse({
"issuer": BASE_URL,
"authorization_endpoint": f"{BASE_URL}/auth/login",
- "authorization_endpoint_simple": f"{BASE_URL}/auth/login-simple", # Simple request endpoint
"token_endpoint": f"{BASE_URL}/token",
- "token_endpoint_simple": f"{BASE_URL}/token-simple", # Simple request endpoint
"registration_endpoint": f"{BASE_URL}/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["read", "search", "openid", "profile", "email"],
- "service_documentation": f"{BASE_URL}/mcp/",
- "preflight_free_endpoints": {
- "authorization": f"{BASE_URL}/auth/login-simple",
- "token": f"{BASE_URL}/token-simple"
- },
- "clerk_optimization": {
- "simple_requests": True,
- "cors_preflight_bypass": True,
- "performance_optimized": True
+ "service_documentation": f"{BASE_URL}/mcp/"
}
})
diff --git a/mcp_server_main.py b/mcp_server_main.py
index 3dd3a58..ccb3c77 100644
--- a/mcp_server_main.py
+++ b/mcp_server_main.py
@@ -243,10 +243,8 @@ def create_app(auth=None):
global app
if auth:
app.auth = auth
- app.name = "Yargı MCP Server"
logger.info("MCP server created with Bearer authentication enabled")
else:
- app.name = "Yargı MCP Server"
logger.info("MCP server created with standard capabilities...")
token_counter = TokenCountingMiddleware()
diff --git a/saidsurucu-yargi-mcp-f5fa007/.dockerignore b/saidsurucu-yargi-mcp-f5fa007/.dockerignore
new file mode 100644
index 0000000..e9f20bb
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/.dockerignore
@@ -0,0 +1,186 @@
+# flyctl launch added from .gitignore
+# Byte-compiled / optimized / DLL files
+**/__pycache__
+**/*.py[cod]
+**/*$py.class
+
+# C extensions
+**/*.so
+
+# Distribution / packaging
+**/.Python
+**/build
+**/develop-eggs
+**/dist
+**/downloads
+**/eggs
+**/.eggs
+**/lib
+**/lib64
+**/parts
+**/sdist
+**/var
+**/wheels
+**/share/python-wheels
+**/*.egg-info
+**/.installed.cfg
+**/*.egg
+**/MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+**/*.manifest
+**/*.spec
+
+# Installer logs
+**/pip-log.txt
+**/pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+**/htmlcov
+**/.tox
+**/.nox
+**/.coverage
+**/.coverage.*
+**/.cache
+**/nosetests.xml
+**/coverage.xml
+**/*.cover
+**/*.py,cover
+**/.hypothesis
+**/.pytest_cache
+**/cover
+
+# Translations
+**/*.mo
+**/*.pot
+
+# Django stuff:
+**/*.log
+**/local_settings.py
+**/db.sqlite3
+**/db.sqlite3-journal
+
+# Flask stuff:
+**/instance
+**/.webassets-cache
+
+# Scrapy stuff:
+**/.scrapy
+
+# Sphinx documentation
+**/docs/_build
+
+# PyBuilder
+**/.pybuilder
+**/target
+
+# Jupyter Notebook
+**/.ipynb_checkpoints
+
+# IPython
+**/profile_default
+**/ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+**/.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+**/__pypackages__
+
+# Celery stuff
+**/celerybeat-schedule
+**/celerybeat.pid
+
+# SageMath parsed files
+**/*.sage.py
+
+# Environments
+**/.env
+**/.venv
+**/env
+**/venv
+**/ENV
+**/env.bak
+**/venv.bak
+
+# Spyder project settings
+**/.spyderproject
+**/.spyproject
+
+# Rope project settings
+**/.ropeproject
+
+# mkdocs documentation
+site
+
+# mypy
+**/.mypy_cache
+**/.dmypy.json
+**/dmypy.json
+
+# Pyre type checker
+**/.pyre
+
+# pytype static type analyzer
+**/.pytype
+
+# Cython debug symbols
+**/cython_debug
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+**/.DS_Store
+**/hello.py
+
+**/*.html
+**/fast-mcp-docs.md
+
+# Debug and test files
+**/debug_*
+**/test_*
+**/CLAUDE.md
+
+# ASGI/Deployment files
+**/ssl
+**/*.pem
+**/*.key
+**/*.crt
+
+# Docker volumes
+**/redis-data
+
+# Production logs
+**/logs/*.log.*
+**/Dockerfile
+**/Dockerfile
+fly.toml
diff --git a/saidsurucu-yargi-mcp-f5fa007/.env.example b/saidsurucu-yargi-mcp-f5fa007/.env.example
new file mode 100644
index 0000000..270a0fb
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/.env.example
@@ -0,0 +1,103 @@
+# OAuth Configuration for Clerk + Google
+# Copy this file to .env and fill in your actual values
+
+# =============================================================================
+# AUTHENTICATION SETTINGS
+# =============================================================================
+
+# Enable/disable authentication (set to "true" to enable OAuth)
+ENABLE_AUTH=false
+
+# =============================================================================
+# CLERK CONFIGURATION
+# =============================================================================
+
+# Clerk API keys (get from https://dashboard.clerk.com/)
+CLERK_SECRET_KEY=sk_test_your_secret_key_here
+CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
+
+# OAuth Redirect URLs
+CLERK_OAUTH_REDIRECT_URL=http://localhost:8000/auth/callback
+CLERK_FRONTEND_URL=http://localhost:3000
+
+# Clerk domain issuer (usually auto-configured)
+CLERK_ISSUER=https://your-clerk-domain.clerk.accounts.dev
+CLERK_DOMAIN=your-clerk-domain
+
+# =============================================================================
+# GOOGLE OAUTH SETTINGS
+# =============================================================================
+# Note: Google OAuth is configured through Clerk dashboard
+# You need to:
+# 1. Go to Clerk Dashboard > Social Connections
+# 2. Enable Google provider
+# 3. Add your Google OAuth client ID and secret
+# 4. Configure redirect URIs in Google Console
+
+# =============================================================================
+# STRIPE CONFIGURATION (for payments/subscriptions)
+# =============================================================================
+
+STRIPE_SECRET=sk_test_your_stripe_secret_key_here
+STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
+
+# =============================================================================
+# SERVER CONFIGURATION
+# =============================================================================
+
+# CORS origins (comma-separated list)
+ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://yourdomain.com
+
+# Server settings
+HOST=0.0.0.0
+PORT=8000
+LOG_LEVEL=info
+
+# Base URL for the application (used for OAuth callbacks and API URLs)
+BASE_URL=http://localhost:8000
+
+# JWT Secret for MCP token generation
+JWT_SECRET_KEY=your_jwt_secret_key_here
+
+# =============================================================================
+# MCP SERVER SETTINGS
+# =============================================================================
+
+# Additional MCP server configuration can go here
+# For example, rate limiting, feature flags, etc.
+
+# Example: Rate limiting
+# MAX_REQUESTS_PER_MINUTE=60
+# BURST_CAPACITY=20
+
+# =============================================================================
+# USAGE INSTRUCTIONS
+# =============================================================================
+
+# 1. Copy this file to .env:
+# cp .env.example .env
+
+# 2. Get Clerk credentials:
+# - Sign up at https://clerk.com/
+# - Create a new application
+# - Go to API Keys tab
+# - Copy Secret Key and Publishable Key
+
+# 3. Configure Google OAuth in Clerk:
+# - In Clerk Dashboard, go to Social Connections
+# - Enable Google provider
+# - Get Google OAuth credentials from Google Console
+# - Add redirect URI: http://localhost:8000/auth/callback
+
+# 4. Update OAuth URLs:
+# - Set CLERK_OAUTH_REDIRECT_URL to your callback URL
+# - Set CLERK_FRONTEND_URL to your frontend application URL
+
+# 5. Enable authentication:
+# - Set ENABLE_AUTH=true
+
+# 6. Test the OAuth flow:
+# - Start server: uvicorn asgi_app:app --reload
+# - Visit: http://localhost:8000/auth/login
+# - Complete OAuth flow with Google
+# - Check: http://localhost:8000/auth/user
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/.gitattributes b/saidsurucu-yargi-mcp-f5fa007/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml b/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml
new file mode 100644
index 0000000..b1924b3
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml
@@ -0,0 +1,37 @@
+name: Publish to PyPI
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch: # Manual trigger for testing
+
+jobs:
+ pypi-publish:
+ name: Upload release to PyPI
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/yargi-mcp
+ permissions:
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build
+
+ - name: Build package
+ run: python -m build
+
+ - name: Publish package to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ password: ${{ secrets.PYPI_API_TOKEN }}
+ skip-existing: true
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/.gitignore b/saidsurucu-yargi-mcp-f5fa007/.gitignore
new file mode 100644
index 0000000..7f3ac3d
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/.gitignore
@@ -0,0 +1,215 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+.DS_Store
+hello.py
+
+*.html
+fast-mcp-docs.md
+
+# Debug and test files
+debug_*
+test_*
+CLAUDE.md
+
+# ASGI/Deployment files
+ssl/
+*.pem
+*.key
+*.crt
+
+# Docker volumes
+redis-data/
+
+# Production logs
+logs/*.log.*
+
+# Remove these lines - we need deployment files in git:
+# Dockerfile - NEEDED for SaaS deployment
+# fly.toml - NEEDED for Fly.io deployment
+# .github/workflows/fly-deploy.yml - NEEDED for GitHub Actions
+
+GEMINI.md
+fly.toml
+scripts/deploy-flyio.sh
+docs/DEPLOYMENT_FLYIO.md
+setup_jwt_template.py
+mcp_server_main.py.backup
+mcp_overhead_content.json
+ANTHROPIC_TEST_README.md
+extract_mcp_overhead.py
+mcp_overhead_content.txt
+mcp_overhead_summary.txt
+run_http_server.py
+run_local_test.py
+
+# MCP overhead analysis files
+mcp_overhead_*.json
+mcp_overhead_*.txt
+mcp_test_results_*.json
+mcp_quick_test_*.json
+
+# General text files (temporary notes, etc)
+*.txt
+analyze_playwright_mcp.py
+measure_mcp_directly.py
+playwright_mcp_overhead.json
+simple_test.py
+analyze_anayasa_html.py
diff --git a/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png b/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png
new file mode 100644
index 0000000..0fa460b
Binary files /dev/null and b/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png differ
diff --git a/saidsurucu-yargi-mcp-f5fa007/Dockerfile b/saidsurucu-yargi-mcp-f5fa007/Dockerfile
new file mode 100644
index 0000000..111f909
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/Dockerfile
@@ -0,0 +1,29 @@
+# -------- BASE IMAGE (includes Chromium & deps) ----------------------------
+FROM mcr.microsoft.com/playwright/python:v1.53.0-noble
+
+# -------- Runtime setup ----------------------------------------------------
+WORKDIR /app
+
+# Copy dependency manifests first for layer-cache
+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 .[asgi,saas]
+
+# Copy application source
+COPY . .
+
+# -------- Environment ------------------------------------------------------
+ENV PYTHONUNBUFFERED=1
+ENV ENABLE_AUTH=true
+ENV PORT=8000
+
+# -------- Health check -----------------------------------------------------
+HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
+ CMD python -c "import httpx, os, sys; r=httpx.get(f'http://localhost:{os.getenv(\"PORT\",\"8000\")}/health'); sys.exit(0 if r.status_code==200 else 1)"
+
+EXPOSE 8000
+
+# -------- Entrypoint -------------------------------------------------------
+CMD ["uvicorn", "asgi_app:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/LICENSE b/saidsurucu-yargi-mcp-f5fa007/LICENSE
new file mode 100644
index 0000000..cdf2fd2
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 saidsurucu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/saidsurucu-yargi-mcp-f5fa007/Procfile b/saidsurucu-yargi-mcp-f5fa007/Procfile
new file mode 100644
index 0000000..2b6b125
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/Procfile
@@ -0,0 +1 @@
+web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/README.md b/saidsurucu-yargi-mcp-f5fa007/README.md
new file mode 100644
index 0000000..570305d
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/README.md
@@ -0,0 +1,272 @@
+# Yargı MCP: Türk Hukuk Kaynakları için MCP Sunucusu
+
+[](https://www.star-history.com/#saidsurucu/yargi-mcp&Date)
+
+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.
+
+
+
+🎯 **Temel Özellikler**
+
+🚀 **YÜKSEK PERFORMANS OPTİMİZASYONU:** Bu MCP sunucusu **%61.8 token azaltma** ile optimize edilmiştir (8,692 token tasarrufu). Claude AI ile daha hızlı yanıt süreleri ve daha verimli etkileşim sağlar.
+
+* Çeşitli Türk hukuk veritabanlarına programatik erişim için standart bir MCP arayüzü.
+* **Kapsamlı Mahkeme Daire/Kurul Filtreleme:** 79 farklı daire/kurul filtreleme seçeneği
+* **Dual/Triple API Desteği:** Her mahkeme için birden fazla API kaynağı ile maksimum kapsama
+* **Kapsamlı Tarih Filtreleme:** Tüm Bedesten API araçlarında ISO 8601 formatında tarih aralığı filtreleme
+* **Kesin Cümle Arama:** Tüm Bedesten API araçlarında çift tırnak ile tam cümle arama desteği
+* Aşağıdaki kurumların kararlarını arama ve getirme yeteneği:
+ * **Yargıtay:** Detaylı kriterlerle karar arama ve karar metinlerini Markdown formatında getirme. **Dual API** (Ana + Bedesten) + **52 Daire/Kurul Filtreleme** + **Tarih & Kesin Cümle Arama** (Hukuk/Ceza Daireleri, Genel Kurullar)
+ * **Danıştay:** Anahtar kelime bazlı ve detaylı kriterlerle karar arama; karar metinlerini Markdown formatında getirme. **Triple API** (Keyword + Detailed + Bedesten) + **27 Daire/Kurul Filtreleme** + **Tarih & Kesin Cümle Arama** (İdari Daireler, Vergi/İdare Kurulları, Askeri Yüksek İdare Mahkemesi)
+ * **Yerel Hukuk Mahkemeleri:** Bedesten API ile yerel hukuk mahkemesi kararlarına erişim + **Tarih & Kesin Cümle Arama**
+ * **İstinaf Hukuk Mahkemeleri:** Bedesten API ile istinaf mahkemesi kararlarına erişim + **Tarih & Kesin Cümle Arama**
+ * **Kanun Yararına Bozma (KYB):** Bedesten API ile olağanüstü kanun yoluna erişim + **Tarih & Kesin Cümle Arama**
+ * **Emsal (UYAP):** Detaylı kriterlerle emsal karar arama ve karar metinlerini Markdown formatında getirme.
+ * **Uyuşmazlık Mahkemesi:** Form tabanlı kriterlerle karar arama ve karar metinlerini (URL ile erişilen) Markdown formatında getirme.
+ * **Anayasa Mahkemesi (Norm Denetimi):** Kapsamlı kriterlerle norm denetimi kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
+ * **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
+ * **KİK (Kamu İhale Kurulu):** Çeşitli kriterlerle Kurul kararlarını arama; uzun karar metinlerini (varsayılan 5.000 karakterlik) sayfalanmış Markdown formatında getirme.
+ * **Rekabet Kurumu:** Çeşitli kriterlerle Kurul kararlarını arama; karar metinlerini Markdown formatında getirme.
+ * **Sayıştay:** 3 karar türü ile kapsamlı denetim kararlarına erişim + **8 Daire Filtreleme** + **Tarih Aralığı & İçerik Arama** (Genel Kurul yorumlayıcı kararları, Temyiz Kurulu itiraz kararları, Daire ilk derece denetim kararları)
+ * **KVKK (Kişisel Verilerin Korunması Kurulu):** Brave Search API ile veri koruma kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme + **Türkçe Arama** + **Site Hedeflemeli Arama** (kvkk.gov.tr kararları)
+ * **BDDK (Bankacılık Düzenleme ve Denetleme Kurumu):** Bankacılık düzenleme kararlarını arama; karar metinlerini Markdown formatında getirme + **Optimized Search** + **"Karar Sayısı" Targeting** + **Spesifik URL Filtreleme** (bddk.org.tr/Mevzuat/DokumanGetir)
+
+* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi.
+* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon.
+* Yargı MCP artık [5ire](https://5ire.app) gibi Claude Desktop haricindeki MCP istemcilerini de destekliyor!
+---
+
+🚀 Claude Haricindeki Modellerle Kullanmak İçin Çok Kolay Kurulum (Örnek: 5ire için)
+
+Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istemcileriyle kullanmak isteyenler içindir.
+
+* **Python Kurulumu:** Sisteminizde Python 3.11 veya üzeri kurulu olmalıdır. Kurulum sırasında "**Add Python to PATH**" (Python'ı PATH'e ekle) seçeneğini işaretlemeyi unutmayın. [Buradan](https://www.python.org/downloads/) indirebilirsiniz.
+* **Git Kurulumu (Windows):** Bilgisayarınıza [git](https://git-scm.com/downloads/win) yazılımını indirip kurun. "Git for Windows/x64 Setup" seçeneğini indirmelisiniz.
+* **`uv` Kurulumu:**
+ * **Windows Kullanıcıları (PowerShell):** Bir CMD ekranı açın ve bu kodu çalıştırın: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
+ * **Mac/Linux Kullanıcıları (Terminal):** Bir Terminal ekranı açın ve bu kodu çalıştırın: `curl -LsSf https://astral.sh/uv/install.sh | sh`
+* **Microsoft Visual C++ Redistributable (Windows):** Bazı Python paketlerinin doğru çalışması için gereklidir. [Buradan](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) indirip kurun.
+* İşletim sisteminize uygun [5ire](https://5ire.app) MCP istemcisini indirip kurun.
+* 5ire'ı açın. **Workspace -> Providers** menüsünden kullanmak istediğiniz LLM servisinin API anahtarını girin.
+* **Tools** menüsüne girin. **+Local** veya **New** yazan butona basın.
+ * **Tool Key:** `yargimcp`
+ * **Name:** `Yargı MCP`
+ * **Command:**
+ ```
+ uvx yargi-mcp
+ ```
+ * **Save** butonuna basarak kaydedin.
+
+* Şimdi **Tools** altında **Yargı MCP**'yi görüyor olmalısınız. Üstüne geldiğinizde sağda çıkan butona tıklayıp etkinleştirin (yeşil ışık yanmalı).
+* Artık Yargı MCP ile konuşabilirsiniz.
+
+
+
+---
+
+⚙️ Claude Desktop Manuel Kurulumu
+
+1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz.
+2. Claude Desktop **Settings -> Developer -> Edit Config**.
+3. Açılan `claude_desktop_config.json` dosyasına `mcpServers` altına ekleyin:
+
+ ```json
+ {
+ "mcpServers": {
+ // ... (varsa diğer sunucularınız) ...
+ "Yargı MCP": {
+ "command": "uvx",
+ "args": [
+ "yargi-mcp"
+ ]
+ }
+ }
+ }
+ ```
+4. Claude Desktop'ı kapatıp yeniden başlatın.
+
+
+
+---
+
+🌟 Gemini CLI ile Kullanım
+
+Yargı MCP'yi Gemini CLI ile kullanmak için:
+
+1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz.
+
+2. **Gemini CLI ayarlarını yapılandırın:**
+
+ Gemini CLI'ın ayar dosyasını düzenleyin:
+ - **macOS/Linux:** `~/.gemini/settings.json`
+ - **Windows:** `%USERPROFILE%\.gemini\settings.json`
+
+ Aşağıdaki `mcpServers` bloğunu ekleyin:
+ ```json
+ {
+ "theme": "Default",
+ "selectedAuthType": "###",
+ "mcpServers": {
+ "yargi_mcp": {
+ "command": "uvx",
+ "args": [
+ "yargi-mcp"
+ ]
+ }
+ }
+ }
+ ```
+
+ **Yapılandırma açıklamaları:**
+ - `"yargi_mcp"`: Sunucunuz için yerel bir isim
+ - `"command"`: `uvx` komutu (uv'nin paket çalıştırma aracı)
+ - `"args"`: GitHub'dan doğrudan Yargı MCP'yi çalıştırmak için gerekli argümanlar
+
+3. **Kullanım:**
+ - Gemini CLI'ı başlatın
+ - Yargı MCP araçları otomatik olarak kullanılabilir olacaktır
+ - Örnek komutlar:
+ - "Yargıtay'ın mülkiyet hakkı ile ilgili son kararlarını ara"
+ - "Danıştay'ın imar planı iptaline ilişkin kararlarını bul"
+ - "Anayasa Mahkemesi'nin ifade özgürlüğü kararlarını getir"
+
+
+
+
+🛠️ Kullanılabilir Araçlar (MCP Tools)
+
+Bu FastMCP sunucusu **19 optimize edilmiş MCP aracı** sunar (token verimliliği için optimize edilmiş):
+
+### **Yargıtay Araçları (Birleşik Bedesten API - Token Optimized)**
+*Not: Yargıtay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
+
+### **Danıştay Araçları (Birleşik Bedesten API - Token Optimized)**
+*Not: Danıştay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir*
+
+### **Birleşik Bedesten API Araçları (5 Mahkeme) - 🚀 TOKEN OPTİMİZE**
+1. `search_bedesten_unified(phrase, court_types, birimAdi, kararTarihiStart, kararTarihiEnd, ...)`: **5 mahkeme türünü** birleşik arama (Yargıtay, Danıştay, Yerel Hukuk, İstinaf Hukuk, KYB) + **79 daire filtreleme** + **Tarih & Kesin Cümle Arama**
+2. `get_bedesten_document_markdown(documentId: str)`: Bedesten API'den herhangi bir belgeyi Markdown formatında getirir (HTML/PDF → Markdown)
+
+### **Emsal Karar Araçları (UYAP)**
+3. `search_emsal_detailed_decisions(keyword, ...)`: Emsal (UYAP) kararlarını detaylı kriterlerle arar.
+4. `get_emsal_document_markdown(id: str)`: Belirli bir Emsal kararının metnini Markdown formatında getirir.
+
+### **Uyuşmazlık Mahkemesi Araçları**
+5. `search_uyusmazlik_decisions(icerik, ...)`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar.
+6. `get_uyusmazlik_document_markdown_from_url(document_url)`: Bir Uyuşmazlık kararını tam URL'sinden alıp Markdown formatında getirir.
+
+### **Anayasa Mahkemesi Araçları (Birleşik API) - 🚀 TOKEN OPTİMİZE**
+7. `search_anayasa_unified(decision_type, keywords_all, ...)`: AYM kararlarını birleşik arama (Norm Denetimi + Bireysel Başvuru) - **4 araç → 2 araç optimizasyonu**
+8. `get_anayasa_document_unified(document_url, page_number)`: AYM kararlarını birleşik belge getirme - **sayfalanmış Markdown** içeriği
+
+### **KİK (Kamu İhale Kurulu) Araçları**
+9. `search_kik_decisions(karar_tipi, ...)`: KİK (Kamu İhale Kurulu) kararlarını arar.
+10. `get_kik_document_markdown(karar_id, page_number)`: Belirli bir KİK kararını, Base64 ile encode edilmiş `karar_id`'sini kullanarak alır ve **sayfalanmış Markdown** içeriğini getirir.
+### **Rekabet Kurumu Araçları**
+ * `search_rekabet_kurumu_decisions(KararTuru: Literal[...], ...) -> RekabetSearchResult`: Rekabet Kurumu kararlarını arar. `KararTuru` için kullanıcı dostu isimler kullanılır (örn: "Birleşme ve Devralma").
+ * `get_rekabet_kurumu_document(karar_id: str, page_number: Optional[int] = 1) -> RekabetDocument`: Belirli bir Rekabet Kurumu kararını `karar_id` ile alır. Kararın PDF formatındaki orijinalinden istenen sayfayı ayıklar ve Markdown formatında döndürür.
+
+
+---
+
+* **Sayıştay Araçları (3 Karar Türü + 8 Daire Filtreleme):**
+ * `search_sayistay_genel_kurul(karar_no, karar_tarih_baslangic, karar_tamami, ...)`: Sayıştay Genel Kurul (yorumlayıcı) kararlarını arar. **Tarih aralığı** (2006-2024) + **İçerik arama** (400 karakter)
+ * `search_sayistay_temyiz_kurulu(ilam_dairesi, kamu_idaresi_turu, temyiz_karar, ...)`: Temyiz Kurulu (itiraz) kararlarını arar. **8 Daire filtreleme** + **Kurum türü** + **Konu sınıflandırması**
+ * `search_sayistay_daire(yargilama_dairesi, web_karar_metni, hesap_yili, ...)`: Daire (ilk derece denetim) kararlarını arar. **8 Daire filtreleme** + **Hesap yılı** + **İçerik arama**
+ * `get_sayistay_genel_kurul_document_markdown(decision_id: str)`: Genel Kurul kararının tam metnini Markdown formatında getirir
+ * `get_sayistay_temyiz_kurulu_document_markdown(decision_id: str)`: Temyiz Kurulu kararının tam metnini Markdown formatında getirir
+ * `get_sayistay_daire_document_markdown(decision_id: str)`: Daire kararının tam metnini Markdown formatında getirir
+
+* **KVKK Araçları (Brave Search API + Türkçe Arama):**
+ * `search_kvkk_decisions(keywords, page, pageSize, ...)`: KVKK (Kişisel Verilerin Korunması Kurulu) kararlarını Brave Search API ile arar. **Türkçe arama** + **Site hedeflemeli** (`site:kvkk.gov.tr "karar özeti"`) + **Sayfalama desteği**
+ * `get_kvkk_document_markdown(decision_url: str, page_number: Optional[int] = 1)`: KVKK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa)
+
+### BDDK Araçları
+ * `search_bddk_decisions(keywords, page)`: BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) kararlarını arar. **"Karar Sayısı" targeting** + **Spesifik URL filtreleme** (`bddk.org.tr/Mevzuat/DokumanGetir`) + **Optimized search**
+ * `get_bddk_document_markdown(document_id: str, page_number: Optional[int] = 1)`: BDDK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa)
+
+
+
+---
+
+
+📊 Kapsamlı İstatistikler & Optimizasyon Başarıları
+
+🚀 **TOKEN OPTİMİZASYON BAŞARISI:**
+- **%61.8 Token Azaltma:** 14,061 → 5,369 tokens (8,692 token tasarrufu)
+- **Hedef Aşım:** 10,000 token hedefini 4,631 token aştık
+- **Daha Hızlı Yanıt:** Claude AI ile optimize edilmiş etkileşim
+- **Korunan İşlevsellik:** %100 özellik desteği devam ediyor
+
+**GENEL İSTATİSTİKLER:**
+- **Toplam Mahkeme/Kurum:** 13 farklı hukuki kurum (KVKK dahil)
+- **Toplam MCP Tool:** 19 optimize edilmiş arama ve belge getirme aracı
+- **Daire/Kurul Filtreleme:** 87 farklı seçenek (52 Yargıtay + 27 Danıştay + 8 Sayıştay)
+- **Tarih Filtreleme:** Birleşik Bedesten API aracında ISO 8601 formatında tam tarih aralığı desteği
+- **Kesin Cümle Arama:** Birleşik Bedesten API aracında çift tırnak ile tam cümle arama (`"\"mülkiyet kararı\""` formatı)
+- **Birleşik API:** 10 ayrı Bedesten aracı → 2 birleşik araç (search_bedesten_unified + get_bedesten_document_markdown)
+- **API Kaynağı:** Dual/Triple API desteği ile maksimum kapsama
+- **Tam Türk Adalet Sistemi:** Yerel mahkemelerden en yüksek mahkemelere kadar
+
+**🏛️ Desteklenen Mahkeme Hiyerarşisi:**
+```
+Yerel Mahkemeler → İstinaf → Yargıtay/Danıştay → Anayasa Mahkemesi
+ ↓ ↓ ↓ ↓
+Bedesten API Bedesten API Dual/Triple API Norm+Bireysel API
++ Tarih + Kesin + Tarih + Kesin + Daire + Tarih + Gelişmiş
+ Cümle Arama Cümle Arama + Kesin Cümle Arama
+```
+
+**⚖️ Kapsamlı Filtreleme Özellikleri:**
+- **Daire Filtreleme:** 79 seçenek (52 Yargıtay + 27 Danıştay)
+ - **Yargıtay:** 52 seçenek (1-23 Hukuk, 1-23 Ceza, Genel Kurullar, Başkanlar Kurulu)
+ - **Danıştay:** 27 seçenek (1-17 Daireler, İdare/Vergi Kurulları, Askeri Mahkemeler)
+- **Tarih Filtreleme:** 5 Bedesten API aracında ISO 8601 formatı (YYYY-MM-DDTHH:MM:SS.000Z)
+ - Tek tarih, tarih aralığı, tek taraflı filtreleme desteği
+ - Yargıtay, Danıştay, Yerel Hukuk, İstinaf Hukuk, KYB kararları
+- **Kesin Cümle Arama:** 5 Bedesten API aracında çift tırnak formatı
+ - Normal arama: `"mülkiyet kararı"` (kelimeler ayrı ayrı)
+ - Kesin arama: `"\"mülkiyet kararı\""` (tam cümle olarak)
+ - Daha kesin sonuçlar için hukuki terimler ve kavramlar
+
+**🔧 OPTİMİZASYON DETAYLARI:**
+- **Anayasa Mahkemesi:** 4 araç → 2 birleşik araç (search_anayasa_unified + get_anayasa_document_unified)
+- **Yargıtay & Danıştay:** Ana API araçları birleşik Bedesten API'ye entegre edildi
+- **Sayıştay:** 6 araç → 2 birleşik araç (search_sayistay_unified + get_sayistay_document_unified)
+- **Parameter Optimizasyonu:** pageSize parametreleri optimize edildi
+- **Açıklama Optimizasyonu:** Uzun açıklamalar kısaltıldı (örn: KIK karar_metni)
+
+
+
+---
+
+
+🌐 Web Service / ASGI Deployment
+
+Yargı MCP artık web servisi olarak da çalıştırılabilir! ASGI desteği sayesinde:
+
+- **Web API olarak erişim**: HTTP endpoint'leri üzerinden MCP araçlarına erişim
+- **Cloud deployment**: Heroku, Railway, Google Cloud Run, AWS Lambda desteği
+- **Docker desteği**: Production-ready Docker container
+- **FastAPI entegrasyonu**: REST API ve interaktif dokümantasyon
+
+**Hızlı başlangıç:**
+```bash
+# ASGI dependencies yükle
+pip install yargi-mcp[asgi]
+
+# Web servisi olarak başlat
+python run_asgi.py
+# veya
+uvicorn asgi_app:app --host 0.0.0.0 --port 8000
+```
+
+Detaylı deployment rehberi için: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)
+
+
+
+---
+
+📜 **Lisans**
+
+Bu proje MIT Lisansı altında lisanslanmıştır. Detaylar için `LICENSE` dosyasına bakınız.
diff --git a/saidsurucu-yargi-mcp-f5fa007/__main__.py b/saidsurucu-yargi-mcp-f5fa007/__main__.py
new file mode 100644
index 0000000..12ab0a0
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/__main__.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+"""Entry point for yargi-mcp package."""
+
+from mcp_server_main import main
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py
new file mode 100644
index 0000000..3d01787
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py
@@ -0,0 +1,355 @@
+# anayasa_mcp_module/bireysel_client.py
+# This client is for Bireysel Başvuru: https://kararlarbilgibankasi.anayasa.gov.tr
+
+import httpx
+from bs4 import BeautifulSoup, Tag
+from typing import Dict, Any, List, Optional, Tuple
+import logging
+import html
+import re
+import io
+from urllib.parse import urlencode, urljoin, quote
+from markitdown import MarkItDown
+import math # For math.ceil for pagination
+
+from .models import (
+ AnayasaBireyselReportSearchRequest,
+ AnayasaBireyselReportDecisionDetail,
+ AnayasaBireyselReportDecisionSummary,
+ AnayasaBireyselReportSearchResult,
+ AnayasaBireyselBasvuruDocumentMarkdown, # Model for Bireysel Başvuru document
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+
+class AnayasaBireyselBasvuruApiClient:
+ BASE_URL = "https://kararlarbilgibankasi.anayasa.gov.tr"
+ SEARCH_PATH = "/Ara"
+ DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page
+
+ def __init__(self, request_timeout: float = 60.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+ },
+ timeout=request_timeout,
+ verify=True,
+ follow_redirects=True
+ )
+
+ def _build_query_params_for_bireysel_report(self, params: AnayasaBireyselReportSearchRequest) -> List[Tuple[str, str]]:
+ query_params: List[Tuple[str, str]] = []
+ query_params.append(("KararBulteni", "1")) # Specific to this report type
+
+ if params.keywords:
+ for kw in params.keywords:
+ query_params.append(("KelimeAra[]", kw))
+
+ if params.page_to_fetch and params.page_to_fetch > 1:
+ query_params.append(("page", str(params.page_to_fetch)))
+
+ return query_params
+
+ async def search_bireysel_basvuru_report(
+ self,
+ params: AnayasaBireyselReportSearchRequest
+ ) -> AnayasaBireyselReportSearchResult:
+ final_query_params = self._build_query_params_for_bireysel_report(params)
+ request_url = self.SEARCH_PATH
+
+ logger.info(f"AnayasaBireyselBasvuruApiClient: Performing Bireysel Başvuru Report search. Path: {request_url}, Params: {final_query_params}")
+
+ try:
+ response = await self.http_client.get(request_url, params=final_query_params)
+ response.raise_for_status()
+ html_content = response.text
+ except httpx.RequestError as e:
+ logger.error(f"AnayasaBireyselBasvuruApiClient: HTTP request error during Bireysel Başvuru Report search: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"AnayasaBireyselBasvuruApiClient: Error processing Bireysel Başvuru Report search request: {e}")
+ raise
+
+ soup = BeautifulSoup(html_content, 'html.parser')
+
+ total_records = None
+ bulunan_karar_div = soup.find("div", class_="bulunankararsayisi")
+ if bulunan_karar_div:
+ match_records = re.search(r'(\d+)\s*Karar Bulundu', bulunan_karar_div.get_text(strip=True))
+ if match_records:
+ total_records = int(match_records.group(1))
+
+ processed_decisions: List[AnayasaBireyselReportDecisionSummary] = []
+
+ report_content_area = soup.find("div", class_="HaberBulteni")
+ if not report_content_area:
+ logger.warning("HaberBulteni div not found, attempting to parse decision divs from the whole page.")
+ report_content_area = soup
+
+ decision_divs = report_content_area.find_all("div", class_="KararBulteniBirKarar")
+ if not decision_divs:
+ logger.warning("No KararBulteniBirKarar divs found.")
+
+
+ for decision_div in decision_divs:
+ title_tag = decision_div.find("h4")
+ title_text = title_tag.get_text(strip=True) if title_tag and title_tag.strong else (title_tag.get_text(strip=True) if title_tag else "")
+
+
+ alti_cizili_div = decision_div.find("div", class_="AltiCizili")
+ ref_no, dec_type, body, app_date, dec_date, url_path = "", "", "", "", "", ""
+ if alti_cizili_div:
+ link_tag = alti_cizili_div.find("a", href=True)
+ if link_tag:
+ ref_no = link_tag.get_text(strip=True)
+ url_path = link_tag['href']
+
+ parts_text = alti_cizili_div.get_text(separator="|", strip=True)
+ parts = [part.strip() for part in parts_text.split("|")]
+
+ # Clean ref_no from the first part if it was extracted from link
+ if ref_no and parts and parts[0].strip().startswith(ref_no):
+ parts[0] = parts[0].replace(ref_no, "").strip()
+ if not parts[0]: parts.pop(0) # Remove empty string if ref_no was the only content
+
+ # Assign parts based on typical order, adjusting for missing ref_no at start
+ current_idx = 0
+ if not ref_no and len(parts) > current_idx and re.match(r"\d+/\d+", parts[current_idx]): # Check if first part is ref_no
+ ref_no = parts[current_idx]
+ current_idx += 1
+
+ dec_type = parts[current_idx] if len(parts) > current_idx else ""
+ current_idx += 1
+ body = parts[current_idx] if len(parts) > current_idx else ""
+ current_idx += 1
+
+ app_date_raw = parts[current_idx] if len(parts) > current_idx else ""
+ current_idx += 1
+ dec_date_raw = parts[current_idx] if len(parts) > current_idx else ""
+
+ if app_date_raw and "Başvuru Tarihi :" in app_date_raw:
+ app_date = app_date_raw.replace("Başvuru Tarihi :", "").strip()
+ elif app_date_raw: # If label is missing but format matches
+ app_date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', app_date_raw)
+ if app_date_match: app_date = app_date_match.group(1)
+
+
+ if dec_date_raw and "Karar Tarihi :" in dec_date_raw:
+ dec_date = dec_date_raw.replace("Karar Tarihi :", "").strip()
+ elif dec_date_raw: # If label is missing but format matches
+ dec_date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', dec_date_raw)
+ if dec_date_match: dec_date = dec_date_match.group(1)
+
+
+ subject_div = decision_div.find(lambda tag: tag.name == 'div' and not tag.has_attr('class') and tag.get_text(strip=True).startswith("BAŞVURU KONUSU :"))
+ subject_text = subject_div.get_text(strip=True).replace("BAŞVURU KONUSU :", "").strip() if subject_div else ""
+
+ details_list: List[AnayasaBireyselReportDecisionDetail] = []
+ karar_detaylari_div = decision_div.find_next_sibling("div", id="KararDetaylari") # Corrected: was KararDetaylari
+ if karar_detaylari_div:
+ table = karar_detaylari_div.find("table", class_="table")
+ if table and table.find("tbody"):
+ for row in table.find("tbody").find_all("tr"):
+ cells = row.find_all("td")
+ if len(cells) == 4: # Hak, Müdahale İddiası, Sonuç, Giderim
+ details_list.append(AnayasaBireyselReportDecisionDetail(
+ hak=cells[0].get_text(strip=True) or "",
+ mudahale_iddiasi=cells[1].get_text(strip=True) or "",
+ sonuc=cells[2].get_text(strip=True) or "",
+ giderim=cells[3].get_text(strip=True) or "",
+ ))
+
+ full_decision_page_url = urljoin(self.BASE_URL, url_path) if url_path else ""
+
+ processed_decisions.append(AnayasaBireyselReportDecisionSummary(
+ title=title_text,
+ decision_reference_no=ref_no,
+ decision_page_url=full_decision_page_url,
+ decision_type_summary=dec_type,
+ decision_making_body=body,
+ application_date_summary=app_date,
+ decision_date_summary=dec_date,
+ application_subject_summary=subject_text,
+ details=details_list
+ ))
+
+ return AnayasaBireyselReportSearchResult(
+ decisions=processed_decisions,
+ total_records_found=total_records,
+ retrieved_page_number=params.page_to_fetch
+ )
+
+ def _convert_html_to_markdown_bireysel(self, full_decision_html_content: str) -> Optional[str]:
+ if not full_decision_html_content:
+ return None
+
+ processed_html = html.unescape(full_decision_html_content)
+ soup = BeautifulSoup(processed_html, "html.parser")
+ html_input_for_markdown = ""
+
+ karar_tab_content = soup.find("div", id="Karar")
+ if karar_tab_content:
+ karar_html_span = karar_tab_content.find("span", class_="kararHtml")
+ if karar_html_span:
+ word_section = karar_html_span.find("div", class_="WordSection1")
+ if word_section:
+ for s in word_section.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'):
+ s.decompose()
+ html_input_for_markdown = str(word_section)
+ else:
+ logger.warning("AnayasaBireyselBasvuruApiClient: WordSection1 not found in span.kararHtml. Using span.kararHtml content.")
+ for s in karar_html_span.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'):
+ s.decompose()
+ html_input_for_markdown = str(karar_html_span)
+ else:
+ logger.warning("AnayasaBireyselBasvuruApiClient: span.kararHtml not found in div#Karar. Using div#Karar content.")
+ for s in karar_tab_content.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'):
+ s.decompose()
+ html_input_for_markdown = str(karar_tab_content)
+ else:
+ logger.warning("AnayasaBireyselBasvuruApiClient: div#Karar (KARAR tab) not found. Trying WordSection1 fallback.")
+ word_section_fallback = soup.find("div", class_="WordSection1")
+ if word_section_fallback:
+ for s in word_section_fallback.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'):
+ s.decompose()
+ html_input_for_markdown = str(word_section_fallback)
+ else:
+ body_tag = soup.find("body")
+ if body_tag:
+ for s in body_tag.select('script, style, .item.col-xs-12.col-sm-12, center:has(b), .banner, .footer, .yazdirmaalani, .filtreler, .menu, .altmenu, .geri, .arabuton, .temizlebutonu, form#KararGetir, .TabBaslik, #KararDetaylari, .share-button-container'):
+ s.decompose()
+ html_input_for_markdown = str(body_tag)
+ else:
+ html_input_for_markdown = processed_html
+
+ markdown_text = None
+ try:
+ # Ensure the content is wrapped in basic HTML structure if it's not already
+ if not html_input_for_markdown.strip().lower().startswith(("
{html_input_for_markdown}"
+ else:
+ html_content = html_input_for_markdown
+
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_content.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ conversion_result = md_converter.convert(html_stream)
+ markdown_text = conversion_result.text_content
+ except Exception as e:
+ logger.error(f"AnayasaBireyselBasvuruApiClient: MarkItDown conversion error: {e}")
+ return markdown_text
+
+ async def get_decision_document_as_markdown(
+ self,
+ document_url_path: str, # e.g. /BB/2021/20295
+ page_number: int = 1
+ ) -> AnayasaBireyselBasvuruDocumentMarkdown:
+ full_url = urljoin(self.BASE_URL, document_url_path)
+ logger.info(f"AnayasaBireyselBasvuruApiClient: Fetching Bireysel Başvuru document for Markdown (page {page_number}) from URL: {full_url}")
+
+ basvuru_no_from_page = None
+ karar_tarihi_from_page = None
+ basvuru_tarihi_from_page = None
+ karari_veren_birim_from_page = None
+ karar_turu_from_page = None
+ resmi_gazete_info_from_page = None
+
+ try:
+ response = await self.http_client.get(full_url)
+ response.raise_for_status()
+ html_content_from_api = response.text
+
+ if not isinstance(html_content_from_api, str) or not html_content_from_api.strip():
+ logger.warning(f"AnayasaBireyselBasvuruApiClient: Received empty HTML from {full_url}.")
+ return AnayasaBireyselBasvuruDocumentMarkdown(
+ source_url=full_url, markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False
+ )
+
+ soup = BeautifulSoup(html_content_from_api, 'html.parser')
+
+ meta_desc_tag = soup.find("meta", attrs={"name": "description"})
+ if meta_desc_tag and meta_desc_tag.get("content"):
+ content = meta_desc_tag["content"]
+ bn_match = re.search(r"B\.\s*No:\s*([\d\/]+)", content)
+ if bn_match: basvuru_no_from_page = bn_match.group(1).strip()
+
+ date_match = re.search(r"(\d{1,2}\/\d{1,2}\/\d{4}),\s*§", content)
+ if date_match: karar_tarihi_from_page = date_match.group(1).strip()
+
+ karar_detaylari_tab = soup.find("div", id="KararDetaylari")
+ if karar_detaylari_tab:
+ table = karar_detaylari_tab.find("table", class_="table")
+ if table:
+ rows = table.find_all("tr")
+ for row in rows:
+ cells = row.find_all("td")
+ if len(cells) == 2:
+ key = cells[0].get_text(strip=True)
+ value = cells[1].get_text(strip=True)
+ if "Kararı Veren Birim" in key: karari_veren_birim_from_page = value
+ elif "Karar Türü (Başvuru Sonucu)" in key: karar_turu_from_page = value
+ elif "Başvuru No" in key and not basvuru_no_from_page: basvuru_no_from_page = value
+ elif "Başvuru Tarihi" in key: basvuru_tarihi_from_page = value
+ elif "Karar Tarihi" in key and not karar_tarihi_from_page: karar_tarihi_from_page = value
+ elif "Resmi Gazete Tarih / Sayı" in key: resmi_gazete_info_from_page = value
+
+ full_markdown_content = self._convert_html_to_markdown_bireysel(html_content_from_api)
+
+ if not full_markdown_content:
+ return AnayasaBireyselBasvuruDocumentMarkdown(
+ source_url=full_url,
+ basvuru_no_from_page=basvuru_no_from_page,
+ karar_tarihi_from_page=karar_tarihi_from_page,
+ basvuru_tarihi_from_page=basvuru_tarihi_from_page,
+ karari_veren_birim_from_page=karari_veren_birim_from_page,
+ karar_turu_from_page=karar_turu_from_page,
+ resmi_gazete_info_from_page=resmi_gazete_info_from_page,
+ markdown_chunk=None,
+ current_page=page_number,
+ total_pages=0,
+ is_paginated=False
+ )
+
+ content_length = len(full_markdown_content)
+ total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE)
+ if total_pages == 0: total_pages = 1
+
+ current_page_clamped = max(1, min(page_number, total_pages))
+ start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ markdown_chunk = full_markdown_content[start_index:end_index]
+
+ return AnayasaBireyselBasvuruDocumentMarkdown(
+ source_url=full_url,
+ basvuru_no_from_page=basvuru_no_from_page,
+ karar_tarihi_from_page=karar_tarihi_from_page,
+ basvuru_tarihi_from_page=basvuru_tarihi_from_page,
+ karari_veren_birim_from_page=karari_veren_birim_from_page,
+ karar_turu_from_page=karar_turu_from_page,
+ resmi_gazete_info_from_page=resmi_gazete_info_from_page,
+ markdown_chunk=markdown_chunk,
+ current_page=current_page_clamped,
+ total_pages=total_pages,
+ is_paginated=(total_pages > 1)
+ )
+
+ except httpx.RequestError as e:
+ logger.error(f"AnayasaBireyselBasvuruApiClient: HTTP error fetching Bireysel Başvuru document from {full_url}: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"AnayasaBireyselBasvuruApiClient: General error processing Bireysel Başvuru document from {full_url}: {e}")
+ raise
+
+ async def close_client_session(self):
+ if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed:
+ await self.http_client.aclose()
+ logger.info("AnayasaBireyselBasvuruApiClient: HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py
new file mode 100644
index 0000000..9e15a48
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py
@@ -0,0 +1,356 @@
+# anayasa_mcp_module/client.py
+# This client is for Norm Denetimi: https://normkararlarbilgibankasi.anayasa.gov.tr
+
+import httpx
+from bs4 import BeautifulSoup
+from typing import Dict, Any, List, Optional, Tuple
+import logging
+import html
+import re
+import io
+from urllib.parse import urlencode, urljoin, quote
+from markitdown import MarkItDown
+import math # For math.ceil for pagination
+
+from .models import (
+ AnayasaNormDenetimiSearchRequest,
+ AnayasaDecisionSummary,
+ AnayasaReviewedNormInfo,
+ AnayasaSearchResult,
+ AnayasaDocumentMarkdown, # Model for Norm Denetimi document
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+class AnayasaMahkemesiApiClient:
+ BASE_URL = "https://normkararlarbilgibankasi.anayasa.gov.tr"
+ SEARCH_PATH_SEGMENT = "Ara"
+ DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page
+
+ def __init__(self, request_timeout: float = 60.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+ },
+ timeout=request_timeout,
+ verify=True,
+ follow_redirects=True
+ )
+
+ def _build_search_query_params_for_aym(self, params: AnayasaNormDenetimiSearchRequest) -> List[Tuple[str, str]]:
+ query_params: List[Tuple[str, str]] = []
+ if params.keywords_all:
+ for kw in params.keywords_all: query_params.append(("KelimeAra[]", kw))
+ if params.keywords_any:
+ for kw in params.keywords_any: query_params.append(("HerhangiBirKelimeAra[]", kw))
+ if params.keywords_exclude:
+ for kw in params.keywords_exclude: query_params.append(("BulunmayanKelimeAra[]", kw))
+ if params.period and params.period and params.period != "ALL": query_params.append(("Donemler_id", params.period))
+ if params.case_number_esas: query_params.append(("EsasNo", params.case_number_esas))
+ if params.decision_number_karar: query_params.append(("KararNo", params.decision_number_karar))
+ if params.first_review_date_start: query_params.append(("IlkIncelemeTarihiIlk", params.first_review_date_start))
+ if params.first_review_date_end: query_params.append(("IlkIncelemeTarihiSon", params.first_review_date_end))
+ if params.decision_date_start: query_params.append(("KararTarihiIlk", params.decision_date_start))
+ if params.decision_date_end: query_params.append(("KararTarihiSon", params.decision_date_end))
+ if params.application_type and params.application_type and params.application_type != "ALL": query_params.append(("BasvuruTurler_id", params.application_type))
+ if params.applicant_general_name: query_params.append(("BasvuranGeneller_id", params.applicant_general_name))
+ if params.applicant_specific_name: query_params.append(("BasvuranOzeller_id", params.applicant_specific_name))
+ if params.attending_members_names:
+ for name in params.attending_members_names: query_params.append(("Uyeler_id[]", name))
+ if params.rapporteur_name: query_params.append(("Raportorler_id", params.rapporteur_name))
+ if params.norm_type and params.norm_type and params.norm_type != "ALL": query_params.append(("NormunTurler_id", params.norm_type))
+ if params.norm_id_or_name: query_params.append(("NormunNumarasiAdlar_id", params.norm_id_or_name))
+ if params.norm_article: query_params.append(("NormunMaddeNumarasi", params.norm_article))
+ if params.review_outcomes:
+ for outcome_val in params.review_outcomes:
+ if outcome_val and outcome_val != "ALL": query_params.append(("IncelemeTuruKararSonuclar_id[]", outcome_val))
+ if params.reason_for_final_outcome and params.reason_for_final_outcome and params.reason_for_final_outcome != "ALL":
+ query_params.append(("KararSonucununGerekcesi", params.reason_for_final_outcome))
+ if params.basis_constitution_article_numbers:
+ for article_no in params.basis_constitution_article_numbers: query_params.append(("DayanakHukmu[]", article_no))
+ if params.official_gazette_date_start: query_params.append(("ResmiGazeteTarihiIlk", params.official_gazette_date_start))
+ if params.official_gazette_date_end: query_params.append(("ResmiGazeteTarihiSon", params.official_gazette_date_end))
+ if params.official_gazette_number_start: query_params.append(("ResmiGazeteSayisiIlk", params.official_gazette_number_start))
+ if params.official_gazette_number_end: query_params.append(("ResmiGazeteSayisiSon", params.official_gazette_number_end))
+ if params.has_press_release and params.has_press_release and params.has_press_release != "ALL": query_params.append(("BasinDuyurusu", params.has_press_release))
+ if params.has_dissenting_opinion and params.has_dissenting_opinion and params.has_dissenting_opinion != "ALL": query_params.append(("KarsiOy", params.has_dissenting_opinion))
+ if params.has_different_reasoning and params.has_different_reasoning and params.has_different_reasoning != "ALL": query_params.append(("FarkliGerekce", params.has_different_reasoning))
+
+ # Add pagination and sorting parameters as query params instead of URL path
+ if params.results_per_page and params.results_per_page != 10:
+ query_params.append(("SatirSayisi", str(params.results_per_page)))
+
+ if params.sort_by_criteria and params.sort_by_criteria != "KararTarihi":
+ query_params.append(("Siralama", params.sort_by_criteria))
+
+ if params.page_to_fetch and params.page_to_fetch > 1:
+ query_params.append(("page", str(params.page_to_fetch)))
+ return query_params
+
+ async def search_norm_denetimi_decisions(
+ self,
+ params: AnayasaNormDenetimiSearchRequest
+ ) -> AnayasaSearchResult:
+ # Use simple /Ara endpoint - the complex path structure seems to cause 404s
+ request_path = f"/{self.SEARCH_PATH_SEGMENT}"
+
+ final_query_params = self._build_search_query_params_for_aym(params)
+ logger.info(f"AnayasaMahkemesiApiClient: Performing Norm Denetimi search. Path: {request_path}, Params: {final_query_params}")
+
+ try:
+ response = await self.http_client.get(request_path, params=final_query_params)
+ response.raise_for_status()
+ html_content = response.text
+ except httpx.RequestError as e:
+ logger.error(f"AnayasaMahkemesiApiClient: HTTP request error during Norm Denetimi search: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"AnayasaMahkemesiApiClient: Error processing Norm Denetimi search request: {e}")
+ raise
+
+ soup = BeautifulSoup(html_content, 'html.parser')
+
+ total_records = None
+ bulunan_karar_div = soup.find("div", class_="bulunankararsayisi")
+ if not bulunan_karar_div: # Fallback for mobile view
+ bulunan_karar_div = soup.find("div", class_="bulunankararsayisiMobil")
+
+ if bulunan_karar_div:
+ match_records = re.search(r'(\d+)\s*Karar Bulundu', bulunan_karar_div.get_text(strip=True))
+ if match_records:
+ total_records = int(match_records.group(1))
+
+ processed_decisions: List[AnayasaDecisionSummary] = []
+ decision_divs = soup.find_all("div", class_="birkarar")
+
+ for decision_div in decision_divs:
+ link_tag = decision_div.find("a", href=True)
+ doc_url_path = link_tag['href'] if link_tag else None
+ decision_page_url_str = urljoin(self.BASE_URL, doc_url_path) if doc_url_path else None
+
+ title_div = decision_div.find("div", class_="bkararbaslik")
+ ek_no_text_raw = title_div.get_text(strip=True, separator=" ").replace('\xa0', ' ') if title_div else ""
+ ek_no_match = re.search(r"(E\.\s*\d+/\d+\s*,\s*K\.\s*\d+/\d+)", ek_no_text_raw)
+ ek_no_text = ek_no_match.group(1) if ek_no_match else ek_no_text_raw.split("Sayılı Karar")[0].strip()
+
+ keyword_count_div = title_div.find("div", class_="BulunanKelimeSayisi") if title_div else None
+ keyword_count_text = keyword_count_div.get_text(strip=True).replace("Bulunan Kelime Sayısı", "").strip() if keyword_count_div else None
+ keyword_count = int(keyword_count_text) if keyword_count_text and keyword_count_text.isdigit() else None
+
+ info_div = decision_div.find("div", class_="kararbilgileri")
+ info_parts = [part.strip() for part in info_div.get_text(separator="|").split("|")] if info_div else []
+
+ app_type_summary = info_parts[0] if len(info_parts) > 0 else None
+ applicant_summary = info_parts[1] if len(info_parts) > 1 else None
+ outcome_summary = info_parts[2] if len(info_parts) > 2 else None
+ dec_date_raw = info_parts[3] if len(info_parts) > 3 else None
+ decision_date_summary = dec_date_raw.replace("Karar Tarihi:", "").strip() if dec_date_raw else None
+
+ reviewed_norms_list: List[AnayasaReviewedNormInfo] = []
+ details_table_container = decision_div.find_next_sibling("div", class_=re.compile(r"col-sm-12")) # The details table is in a sibling div
+ if details_table_container:
+ details_table = details_table_container.find("table", class_="table")
+ if details_table and details_table.find("tbody"):
+ for row in details_table.find("tbody").find_all("tr"):
+ cells = row.find_all("td")
+ if len(cells) == 6:
+ reviewed_norms_list.append(AnayasaReviewedNormInfo(
+ norm_name_or_number=cells[0].get_text(strip=True) or None,
+ article_number=cells[1].get_text(strip=True) or None,
+ review_type_and_outcome=cells[2].get_text(strip=True) or None,
+ outcome_reason=cells[3].get_text(strip=True) or None,
+ basis_constitution_articles_cited=[a.strip() for a in cells[4].get_text(strip=True).split(',') if a.strip()] if cells[4].get_text(strip=True) else [],
+ postponement_period=cells[5].get_text(strip=True) or None
+ ))
+
+ processed_decisions.append(AnayasaDecisionSummary(
+ decision_reference_no=ek_no_text,
+ decision_page_url=decision_page_url_str,
+ keywords_found_count=keyword_count,
+ application_type_summary=app_type_summary,
+ applicant_summary=applicant_summary,
+ decision_outcome_summary=outcome_summary,
+ decision_date_summary=decision_date_summary,
+ reviewed_norms=reviewed_norms_list
+ ))
+
+ return AnayasaSearchResult(
+ decisions=processed_decisions,
+ total_records_found=total_records,
+ retrieved_page_number=params.page_to_fetch
+ )
+
+ def _convert_html_to_markdown_norm_denetimi(self, full_decision_html_content: str) -> Optional[str]:
+ """Converts direct HTML content from an Anayasa Mahkemesi Norm Denetimi decision page to Markdown."""
+ if not full_decision_html_content:
+ return None
+
+ processed_html = html.unescape(full_decision_html_content)
+ soup = BeautifulSoup(processed_html, "html.parser")
+ html_input_for_markdown = ""
+
+ karar_tab_content = soup.find("div", id="Karar") # "KARAR" tab content
+ if karar_tab_content:
+ karar_metni_div = karar_tab_content.find("div", class_="KararMetni")
+ if karar_metni_div:
+ # Remove scripts and styles
+ for script_tag in karar_metni_div.find_all("script"): script_tag.decompose()
+ for style_tag in karar_metni_div.find_all("style"): style_tag.decompose()
+ # Remove "Künye Kopyala" button and other non-content divs
+ for item_div in karar_metni_div.find_all("div", class_="item col-sm-12"): item_div.decompose()
+ for modal_div in karar_metni_div.find_all("div", class_="modal fade"): modal_div.decompose() # If any modals
+
+ word_section = karar_metni_div.find("div", class_="WordSection1")
+ html_input_for_markdown = str(word_section) if word_section else str(karar_metni_div)
+ else:
+ html_input_for_markdown = str(karar_tab_content)
+ else:
+ # Fallback if specific structure is not found
+ word_section_fallback = soup.find("div", class_="WordSection1")
+ if word_section_fallback:
+ html_input_for_markdown = str(word_section_fallback)
+ else:
+ # Last resort: use the whole body or the raw HTML
+ body_tag = soup.find("body")
+ html_input_for_markdown = str(body_tag) if body_tag else processed_html
+
+ markdown_text = None
+ try:
+ # Ensure the content is wrapped in basic HTML structure if it's not already
+ if not html_input_for_markdown.strip().lower().startswith(("{html_input_for_markdown}"
+ else:
+ html_content = html_input_for_markdown
+
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_content.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ conversion_result = md_converter.convert(html_stream)
+ markdown_text = conversion_result.text_content
+ except Exception as e:
+ logger.error(f"AnayasaMahkemesiApiClient: MarkItDown conversion error: {e}")
+ return markdown_text
+
+ async def get_decision_document_as_markdown(
+ self,
+ document_url: str,
+ page_number: int = 1
+ ) -> AnayasaDocumentMarkdown:
+ """
+ Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision,
+ converts its content to Markdown, and returns the requested page/chunk.
+ """
+ full_url = urljoin(self.BASE_URL, document_url) if not document_url.startswith("http") else document_url
+ logger.info(f"AnayasaMahkemesiApiClient: Fetching Norm Denetimi document for Markdown (page {page_number}) from URL: {full_url}")
+
+ decision_ek_no_from_page = None
+ decision_date_from_page = None
+ official_gazette_from_page = None
+
+ try:
+ # Use a new client instance for document fetching if headers/timeout needs to be different,
+ # or reuse self.http_client if settings are compatible. For now, self.http_client.
+ get_response = await self.http_client.get(full_url, headers={"Accept": "text/html"})
+ get_response.raise_for_status()
+ html_content_from_api = get_response.text
+
+ if not isinstance(html_content_from_api, str) or not html_content_from_api.strip():
+ logger.warning(f"AnayasaMahkemesiApiClient: Received empty or non-string HTML from URL {full_url}.")
+ return AnayasaDocumentMarkdown(
+ source_url=full_url, markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False
+ )
+
+ # Extract metadata from the page content (E.K. No, Date, RG)
+ soup = BeautifulSoup(html_content_from_api, "html.parser")
+ karar_metni_div = soup.find("div", class_="KararMetni") # Usually within div#Karar
+ if not karar_metni_div: # Fallback if not in KararMetni
+ karar_metni_div = soup.find("div", class_="WordSection1")
+
+ # Initialize with empty string defaults
+ decision_ek_no_from_page = ""
+ decision_date_from_page = ""
+ official_gazette_from_page = ""
+
+ if karar_metni_div:
+ # Attempt to find E.K. No (Esas No, Karar No)
+ # Norm Denetimi pages often have this in bold tags directly or in the WordSection1
+ # Look for patterns like "Esas No.: YYYY/NN" and "Karar No.: YYYY/NN"
+
+ esas_no_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Esas No.:" in tag.find("b").get_text())
+ karar_no_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Karar No.:" in tag.find("b").get_text())
+ karar_tarihi_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Karar tarihi:" in tag.find("b").get_text()) # Less common on Norm pages
+ resmi_gazete_tag = karar_metni_div.find(lambda tag: tag.name == "p" and ("Resmî Gazete tarih ve sayısı:" in tag.get_text() or "Resmi Gazete tarih/sayı:" in tag.get_text()))
+
+
+ if esas_no_tag and esas_no_tag.find("b") and karar_no_tag and karar_no_tag.find("b"):
+ esas_str = esas_no_tag.find("b").get_text(strip=True).replace('Esas No.:', '').strip()
+ karar_str = karar_no_tag.find("b").get_text(strip=True).replace('Karar No.:', '').strip()
+ decision_ek_no_from_page = f"E.{esas_str}, K.{karar_str}"
+
+ if karar_tarihi_tag and karar_tarihi_tag.find("b"):
+ decision_date_from_page = karar_tarihi_tag.find("b").get_text(strip=True).replace("Karar tarihi:", "").strip()
+ elif karar_metni_div: # Fallback for Karar Tarihi if not in specific tag
+ date_match = re.search(r"Karar Tarihi\s*:\s*([\d\.]+)", karar_metni_div.get_text()) # Norm pages often use DD.MM.YYYY
+ if date_match: decision_date_from_page = date_match.group(1).strip()
+
+
+ if resmi_gazete_tag:
+ # Try to get the bold part first if it exists
+ bold_rg_tag = resmi_gazete_tag.find("b")
+ rg_text_content = bold_rg_tag.get_text(strip=True) if bold_rg_tag else resmi_gazete_tag.get_text(strip=True)
+ official_gazette_from_page = rg_text_content.replace("Resmî Gazete tarih ve sayısı:", "").replace("Resmi Gazete tarih/sayı:", "").strip()
+
+
+ full_markdown_content = self._convert_html_to_markdown_norm_denetimi(html_content_from_api)
+
+ if not full_markdown_content:
+ return AnayasaDocumentMarkdown(
+ source_url=full_url,
+ decision_reference_no_from_page=decision_ek_no_from_page,
+ decision_date_from_page=decision_date_from_page,
+ official_gazette_info_from_page=official_gazette_from_page,
+ markdown_chunk=None,
+ current_page=page_number,
+ total_pages=0,
+ is_paginated=False
+ )
+
+ content_length = len(full_markdown_content)
+ total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE)
+ if total_pages == 0: total_pages = 1
+
+ current_page_clamped = max(1, min(page_number, total_pages))
+ start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ markdown_chunk = full_markdown_content[start_index:end_index]
+
+ return AnayasaDocumentMarkdown(
+ source_url=full_url,
+ decision_reference_no_from_page=decision_ek_no_from_page,
+ decision_date_from_page=decision_date_from_page,
+ official_gazette_info_from_page=official_gazette_from_page,
+ markdown_chunk=markdown_chunk,
+ current_page=current_page_clamped,
+ total_pages=total_pages,
+ is_paginated=(total_pages > 1)
+ )
+
+ except httpx.RequestError as e:
+ logger.error(f"AnayasaMahkemesiApiClient: HTTP error fetching Norm Denetimi document from {full_url}: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"AnayasaMahkemesiApiClient: General error processing Norm Denetimi document from {full_url}: {e}")
+ raise
+
+ async def close_client_session(self):
+ if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed:
+ await self.http_client.aclose()
+ logger.info("AnayasaMahkemesiApiClient (Norm Denetimi): HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py
new file mode 100644
index 0000000..f5e74e1
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py
@@ -0,0 +1,230 @@
+# anayasa_mcp_module/models.py
+
+from pydantic import BaseModel, Field, HttpUrl
+from typing import List, Optional, Dict, Any, Literal
+from enum import Enum
+
+# --- Enums (AnayasaDonemEnum, etc. - same as before) ---
+class AnayasaDonemEnum(str, Enum):
+ TUMU = "ALL"
+ DONEM_1961 = "1"
+ DONEM_1982 = "2"
+
+
+class AnayasaVarYokEnum(str, Enum):
+ TUMU = "ALL"
+ YOK = "0"
+ VAR = "1"
+
+
+class AnayasaIncelemeSonucuEnum(str, Enum):
+ TUMU = "ALL"
+ ESAS_ACILMAMIS_SAYILMA = "1"
+ ESAS_IPTAL = "2"
+ ESAS_KARAR_YER_OLMADIGI = "3"
+ ESAS_RET = "4"
+ ILK_ACILMAMIS_SAYILMA = "5"
+ ILK_ISIN_GERI_CEVRILMESI = "6"
+ ILK_KARAR_YER_OLMADIGI = "7"
+ ILK_RET = "8"
+ KANUN_6216_M43_4_IPTAL = "12"
+
+class AnayasaSonucGerekcesiEnum(str, Enum):
+ TUMU = "ALL"
+ ANAYASAYA_AYKIRI_DEGIL = "29"
+ ANAYASAYA_ESAS_YONUNDEN_AYKIRILIK = "1"
+ ANAYASAYA_ESAS_YONUNDEN_UYGUNLUK = "2"
+ ANAYASAYA_SEKIL_ESAS_UYGUNLUK = "30"
+ ANAYASAYA_SEKIL_YONUNDEN_AYKIRILIK = "3"
+ ANAYASAYA_SEKIL_YONUNDEN_UYGUNLUK = "4"
+ AYKIRILIK_ANAYASAYA_ESAS_YONUNDEN_DUPLICATE = "27"
+ BASVURU_KARARI = "5"
+ DENETIM_DISI = "6"
+ DIGER_GEREKCE_1 = "7"
+ DIGER_GEREKCE_2 = "8"
+ EKSIKLIGIN_GIDERILMEMESI = "9"
+ GEREKCE = "10"
+ GOREV = "11"
+ GOREV_YETKI = "12"
+ GOREVLI_MAHKEME = "13"
+ GORULMEKTE_OLAN_DAVA = "14"
+ MAHKEME = "15"
+ NORMDA_DEGISIKLIK_YAPILMASI = "16"
+ NORMUN_YURURLUKTEN_KALDIRILMASI = "17"
+ ON_YIL_YASAGI = "18"
+ SURE = "19"
+ USULE_UYMAMA = "20"
+ UYGULANACAK_NORM = "21"
+ UYGULANAMAZ_HALE_GELME = "22"
+ YETKI = "23"
+ YETKI_SURE = "24"
+ YOK_HUKMUNDE_OLMAMA = "25"
+ YOKLUK = "26"
+# --- End Enums ---
+
+class AnayasaNormDenetimiSearchRequest(BaseModel):
+ """Model for Anayasa Mahkemesi (Norm Denetimi) search request for the MCP tool."""
+ keywords_all: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).")
+ keywords_any: Optional[List[str]] = Field(default_factory=list, description="Keywords for OR logic (HerhangiBirKelimeAra[]).")
+ keywords_exclude: Optional[List[str]] = Field(default_factory=list, description="Keywords to exclude (BulunmayanKelimeAra[]).")
+ period: Optional[Literal["ALL", "1", "2"]] = Field(default="ALL", description="Constitutional period (Donemler_id).")
+ case_number_esas: str = Field("", description="Case registry number (EsasNo), e.g., '2023/123'.")
+ decision_number_karar: str = Field("", description="Decision number (KararNo), e.g., '2023/456'.")
+ first_review_date_start: str = Field("", description="First review start date (IlkIncelemeTarihiIlk), format DD/MM/YYYY.")
+ first_review_date_end: str = Field("", description="First review end date (IlkIncelemeTarihiSon), format DD/MM/YYYY.")
+ decision_date_start: str = Field("", description="Decision start date (KararTarihiIlk), format DD/MM/YYYY.")
+ decision_date_end: str = Field("", description="Decision end date (KararTarihiSon), format DD/MM/YYYY.")
+ application_type: Optional[Literal["ALL", "1", "2", "3"]] = Field(default="ALL", description="Type of application (BasvuruTurler_id).")
+ applicant_general_name: str = Field("", description="General applicant name (BasvuranGeneller_id).")
+ applicant_specific_name: str = Field("", description="Specific applicant name (BasvuranOzeller_id).")
+ official_gazette_date_start: str = Field("", description="Official Gazette start date (ResmiGazeteTarihiIlk), format DD/MM/YYYY.")
+ official_gazette_date_end: str = Field("", description="Official Gazette end date (ResmiGazeteTarihiSon), format DD/MM/YYYY.")
+ official_gazette_number_start: str = Field("", description="Official Gazette starting number (ResmiGazeteSayisiIlk).")
+ official_gazette_number_end: str = Field("", description="Official Gazette ending number (ResmiGazeteSayisiSon).")
+ has_press_release: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Press release available (BasinDuyurusu).")
+ has_dissenting_opinion: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Dissenting opinion exists (KarsiOy).")
+ has_different_reasoning: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Different reasoning exists (FarkliGerekce).")
+ attending_members_names: Optional[List[str]] = Field(default_factory=list, description="List of attending members' exact names (Uyeler_id[]).")
+ rapporteur_name: str = Field("", description="Rapporteur's exact name (Raportorler_id).")
+ norm_type: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"]] = Field(default="ALL", description="Type of the reviewed norm (NormunTurler_id).")
+ norm_id_or_name: str = Field("", description="Number or name of the norm (NormunNumarasiAdlar_id).")
+ norm_article: str = Field("", description="Article number of the norm (NormunMaddeNumarasi).")
+ review_outcomes: Optional[List[Literal["1", "2", "3", "4", "5", "6", "7", "8", "12"]]] = Field(default_factory=list, description="List of review types and outcomes (IncelemeTuruKararSonuclar_id[]).")
+ reason_for_final_outcome: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "29", "30"]] = Field(default="ALL", description="Main reason for the decision outcome (KararSonucununGerekcesi).")
+ basis_constitution_article_numbers: Optional[List[str]] = Field(default_factory=list, description="List of supporting Constitution article numbers (DayanakHukmu[]).")
+ results_per_page: int = Field(10, ge=1, le=10, description="Results per page.")
+ page_to_fetch: int = Field(1, ge=1, description="Page number to fetch for results list.")
+ sort_by_criteria: str = Field("KararTarihi", description="Sort criteria. Options: 'KararTarihi', 'YayinTarihi', 'Toplam' (keyword count).")
+
+class AnayasaReviewedNormInfo(BaseModel):
+ """Details of a norm reviewed within an AYM decision summary."""
+ norm_name_or_number: str = Field("", description="Norm name or number")
+ article_number: str = Field("", description="Article number")
+ review_type_and_outcome: str = Field("", description="Review type and outcome")
+ outcome_reason: str = Field("", description="Outcome reason")
+ basis_constitution_articles_cited: List[str] = Field(default_factory=list)
+ postponement_period: str = Field("", description="Postponement period")
+
+class AnayasaDecisionSummary(BaseModel):
+ """Model for a single Anayasa Mahkemesi (Norm Denetimi) decision summary from search results."""
+ decision_reference_no: str = Field("", description="Decision reference number")
+ decision_page_url: str = Field("", description="Decision page URL")
+ keywords_found_count: Optional[int] = Field(0, description="Keywords found count")
+ application_type_summary: str = Field("", description="Application type summary")
+ applicant_summary: str = Field("", description="Applicant summary")
+ decision_outcome_summary: str = Field("", description="Decision outcome summary")
+ decision_date_summary: str = Field("", description="Decision date summary")
+ reviewed_norms: List[AnayasaReviewedNormInfo] = Field(default_factory=list)
+
+class AnayasaSearchResult(BaseModel):
+ """Model for the overall search result for Anayasa Mahkemesi Norm Denetimi decisions."""
+ decisions: List[AnayasaDecisionSummary]
+ total_records_found: int = Field(0, description="Total records found")
+ retrieved_page_number: int = Field(1, description="Retrieved page number")
+
+class AnayasaDocumentMarkdown(BaseModel):
+ """
+ Model for an Anayasa Mahkemesi (Norm Denetimi) decision document, containing a chunk of Markdown content
+ and pagination information.
+ """
+ source_url: HttpUrl
+ decision_reference_no_from_page: str = Field("", description="E.K. No parsed from the document page.")
+ decision_date_from_page: str = Field("", description="Decision date parsed from the document page.")
+ official_gazette_info_from_page: str = Field("", description="Official Gazette info parsed from the document page.")
+ markdown_chunk: str = Field("", description="A 5,000 character chunk of the Markdown content.") # Corrected chunk size
+ current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
+ total_pages: int = Field(description="Total number of pages for the full markdown content.")
+ is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
+
+
+# --- Models for Anayasa Mahkemesi - Bireysel Başvuru Karar Raporu ---
+
+class AnayasaBireyselReportSearchRequest(BaseModel):
+ """Model for Anayasa Mahkemesi (Bireysel Başvuru) 'Karar Arama Raporu' search request."""
+ keywords: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).")
+ page_to_fetch: int = Field(1, ge=1, description="Page number to fetch for the report (page). Default is 1.")
+
+class AnayasaBireyselReportDecisionDetail(BaseModel):
+ """Details of a specific right/claim within a Bireysel Başvuru decision summary in a report."""
+ hak: str = Field("", description="İhlal edildiği iddia edilen hak (örneğin, Mülkiyet hakkı).")
+ mudahale_iddiasi: str = Field("", description="İhlale neden olan müdahale iddiası.")
+ sonuc: str = Field("", description="İnceleme sonucu (örneğin, İhlal, Düşme).")
+ giderim: str = Field("", description="Kararlaştırılan giderim (örneğin, Yeniden yargılama).")
+
+class AnayasaBireyselReportDecisionSummary(BaseModel):
+ """Model for a single Anayasa Mahkemesi (Bireysel Başvuru) decision summary from a 'Karar Arama Raporu'."""
+ title: str = Field("", description="Başvurunun başlığı (e.g., 'HASAN DURMUŞ Başvurusuna İlişkin Karar').")
+ decision_reference_no: str = Field("", description="Başvuru Numarası (e.g., '2019/19126').")
+ decision_page_url: str = Field("", description="URL to the full decision page.")
+ decision_type_summary: str = Field("", description="Karar Türü (Başvuru Sonucu) (e.g., 'Esas (İhlal)').")
+ decision_making_body: str = Field("", description="Kararı Veren Birim (e.g., 'Genel Kurul', 'Birinci Bölüm').")
+ application_date_summary: str = Field("", description="Başvuru Tarihi (DD/MM/YYYY).")
+ decision_date_summary: str = Field("", description="Karar Tarihi (DD/MM/YYYY).")
+ application_subject_summary: str = Field("", description="Başvuru konusunun özeti.")
+ details: List[AnayasaBireyselReportDecisionDetail] = Field(default_factory=list, description="İncelenen haklar ve sonuçlarına ilişkin detaylar.")
+
+class AnayasaBireyselReportSearchResult(BaseModel):
+ """Model for the overall search result for Anayasa Mahkemesi 'Karar Arama Raporu'."""
+ decisions: List[AnayasaBireyselReportDecisionSummary]
+ total_records_found: int = Field(0, description="Raporda bulunan toplam karar sayısı.")
+ retrieved_page_number: int = Field(description="Alınan rapor sayfa numarası.")
+
+
+class AnayasaBireyselBasvuruDocumentMarkdown(BaseModel):
+ """
+ Model for an Anayasa Mahkemesi (Bireysel Başvuru) decision document, containing a chunk of Markdown content
+ and pagination information. Fetched from /BB/YYYY/NNNN paths.
+ """
+ source_url: HttpUrl
+ basvuru_no_from_page: Optional[str] = Field(None, description="Başvuru Numarası (B.No) parsed from the document page.")
+ karar_tarihi_from_page: Optional[str] = Field(None, description="Decision date parsed from the document page.")
+ basvuru_tarihi_from_page: Optional[str] = Field(None, description="Application date parsed from the document page.")
+ karari_veren_birim_from_page: Optional[str] = Field(None, description="Deciding body (Bölüm/Genel Kurul) parsed from the document page.")
+ karar_turu_from_page: Optional[str] = Field(None, description="Decision type (Başvuru Sonucu) parsed from the document page.")
+ resmi_gazete_info_from_page: Optional[str] = Field(None, description="Official Gazette info parsed from the document page, if available.")
+ markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content.")
+ current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
+ total_pages: int = Field(description="Total number of pages for the full markdown content.")
+ is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
+
+# --- End Models for Bireysel Başvuru ---
+
+# --- Unified Models ---
+class AnayasaUnifiedSearchRequest(BaseModel):
+ """Unified search request for both Norm Denetimi and Bireysel Başvuru."""
+ decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi or bireysel_basvuru")
+
+ # Common parameters
+ keywords: List[str] = Field(default_factory=list, description="Keywords to search for")
+ page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)")
+ results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)")
+
+ # Norm Denetimi specific parameters (ignored for bireysel_basvuru)
+ keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)")
+ keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)")
+ decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi")
+ application_date_start: str = Field("", description="Application start date (norm_denetimi only)")
+ application_date_end: str = Field("", description="Application end date (norm_denetimi only)")
+
+ # Bireysel Başvuru specific parameters (ignored for norm_denetimi)
+ decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)")
+ decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)")
+ norm_type: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"] = Field("ALL", description="Norm type (bireysel_basvuru only)")
+ subject_category: str = Field("", description="Subject category (bireysel_basvuru only)")
+
+class AnayasaUnifiedSearchResult(BaseModel):
+ """Unified search result containing decisions from either system."""
+ decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of decisions returned")
+ decisions: List[Dict[str, Any]] = Field(default_factory=list, description="Decision list (structure varies by type)")
+ total_records_found: int = Field(0, description="Total number of records found")
+ retrieved_page_number: int = Field(1, description="Page number that was retrieved")
+
+class AnayasaUnifiedDocumentMarkdown(BaseModel):
+ """Unified document model for both Norm Denetimi and Bireysel Başvuru."""
+ decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of document")
+ source_url: HttpUrl = Field(..., description="Source URL of the document")
+ document_data: Dict[str, Any] = Field(default_factory=dict, description="Document content and metadata")
+ markdown_chunk: Optional[str] = Field(None, description="Markdown content chunk")
+ current_page: int = Field(1, description="Current page number")
+ total_pages: int = Field(1, description="Total number of pages")
+ is_paginated: bool = Field(False, description="Whether document is paginated")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py
new file mode 100644
index 0000000..1319b85
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py
@@ -0,0 +1,122 @@
+# anayasa_mcp_module/unified_client.py
+# Unified client for both Norm Denetimi and Bireysel Başvuru
+
+import logging
+from typing import Optional
+from urllib.parse import urlparse
+
+from .models import (
+ AnayasaUnifiedSearchRequest,
+ AnayasaUnifiedSearchResult,
+ AnayasaUnifiedDocumentMarkdown,
+ # Removed AnayasaDecisionTypeEnum - now using string literals
+ AnayasaNormDenetimiSearchRequest,
+ AnayasaBireyselReportSearchRequest
+)
+from .client import AnayasaMahkemesiApiClient
+from .bireysel_client import AnayasaBireyselBasvuruApiClient
+
+logger = logging.getLogger(__name__)
+
+class AnayasaUnifiedClient:
+ """Unified client that handles both Norm Denetimi and Bireysel Başvuru searches."""
+
+ def __init__(self, request_timeout: float = 60.0):
+ self.norm_client = AnayasaMahkemesiApiClient(request_timeout)
+ self.bireysel_client = AnayasaBireyselBasvuruApiClient(request_timeout)
+
+ async def search_unified(self, params: AnayasaUnifiedSearchRequest) -> AnayasaUnifiedSearchResult:
+ """Unified search that routes to appropriate client based on decision_type."""
+
+ if params.decision_type == "norm_denetimi":
+ # Convert to norm denetimi request
+ norm_params = AnayasaNormDenetimiSearchRequest(
+ keywords_all=params.keywords_all or params.keywords,
+ keywords_any=params.keywords_any,
+ application_type=params.decision_type_norm,
+ page_to_fetch=params.page_to_fetch,
+ results_per_page=params.results_per_page
+ )
+
+ result = await self.norm_client.search_norm_denetimi_decisions(norm_params)
+
+ # Convert to unified format
+ decisions_list = [decision.model_dump() for decision in result.decisions]
+
+ return AnayasaUnifiedSearchResult(
+ decision_type="norm_denetimi",
+ decisions=decisions_list,
+ total_records_found=result.total_records_found,
+ retrieved_page_number=result.retrieved_page_number
+ )
+
+ elif params.decision_type == "bireysel_basvuru":
+ # Convert to bireysel başvuru request
+ bireysel_params = AnayasaBireyselReportSearchRequest(
+ keywords=params.keywords,
+ decision_start_date=params.decision_start_date,
+ decision_end_date=params.decision_end_date,
+ norm_type=params.norm_type,
+ subject_category=params.subject_category,
+ page_to_fetch=params.page_to_fetch,
+ results_per_page=params.results_per_page
+ )
+
+ result = await self.bireysel_client.search_bireysel_basvuru_report(bireysel_params)
+
+ # Convert to unified format
+ decisions_list = [decision.model_dump() for decision in result.decisions]
+
+ return AnayasaUnifiedSearchResult(
+ decision_type="bireysel_basvuru",
+ decisions=decisions_list,
+ total_records_found=result.total_records_found,
+ retrieved_page_number=result.retrieved_page_number
+ )
+
+ else:
+ raise ValueError(f"Unsupported decision type: {params.decision_type}")
+
+ async def get_document_unified(self, document_url: str, page_number: int = 1) -> AnayasaUnifiedDocumentMarkdown:
+ """Unified document retrieval that auto-detects the appropriate client."""
+
+ # Auto-detect decision type based on URL
+ parsed_url = urlparse(document_url)
+
+ if "normkararlarbilgibankasi" in parsed_url.netloc or "/ND/" in document_url:
+ # Norm Denetimi document
+ result = await self.norm_client.get_decision_document_as_markdown(document_url, page_number)
+
+ return AnayasaUnifiedDocumentMarkdown(
+ decision_type="norm_denetimi",
+ source_url=result.source_url,
+ document_data=result.model_dump(),
+ markdown_chunk=result.markdown_chunk,
+ current_page=result.current_page,
+ total_pages=result.total_pages,
+ is_paginated=result.is_paginated
+ )
+
+ elif "kararlarbilgibankasi" in parsed_url.netloc or "/BB/" in document_url:
+ # Bireysel Başvuru document
+ result = await self.bireysel_client.get_decision_document_as_markdown(document_url, page_number)
+
+ return AnayasaUnifiedDocumentMarkdown(
+ decision_type="bireysel_basvuru",
+ source_url=result.source_url,
+ document_data=result.model_dump(),
+ markdown_chunk=result.markdown_chunk,
+ current_page=result.current_page,
+ total_pages=result.total_pages,
+ is_paginated=result.is_paginated
+ )
+
+ else:
+ raise ValueError(f"Cannot determine document type from URL: {document_url}")
+
+ async def close_client_session(self):
+ """Close both client sessions."""
+ if hasattr(self.norm_client, 'close_client_session'):
+ await self.norm_client.close_client_session()
+ if hasattr(self.bireysel_client, 'close_client_session'):
+ await self.bireysel_client.close_client_session()
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/asgi_app.py b/saidsurucu-yargi-mcp-f5fa007/asgi_app.py
new file mode 100644
index 0000000..ba5a1f4
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/asgi_app.py
@@ -0,0 +1,566 @@
+"""
+ASGI application for Yargı MCP Server
+
+This module provides ASGI/HTTP access to the Yargı MCP server,
+allowing it to be deployed as a web service with FastAPI wrapper
+for Stripe webhook integration.
+
+Usage:
+ uvicorn asgi_app:app --host 0.0.0.0 --port 8000
+"""
+
+import os
+import time
+import logging
+from datetime import datetime, timedelta
+from fastapi import FastAPI, Request, HTTPException, Query
+from fastapi.responses import JSONResponse, HTMLResponse
+from fastapi.exception_handlers import http_exception_handler
+from starlette.middleware import Middleware
+from starlette.middleware.cors import CORSMiddleware
+from starlette.responses import Response
+from starlette.requests import Request as StarletteRequest
+
+# Import the MCP app creator function
+from mcp_server_main import create_app
+
+# Import Stripe webhook router
+from stripe_webhook import router as stripe_router
+
+# Import simplified MCP Auth HTTP adapter
+from mcp_auth_http_simple import router as mcp_auth_router
+
+# OAuth configuration from environment variables
+CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
+BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
+
+# Setup logging
+logger = logging.getLogger(__name__)
+
+# Configure CORS and Auth middleware
+cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
+
+# Import FastMCP Bearer Auth Provider
+from fastmcp.server.auth import BearerAuthProvider
+from fastmcp.server.auth.providers.bearer import RSAKeyPair
+
+# Clerk JWT configuration for Bearer token validation
+CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
+CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
+CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
+
+# Configure Bearer token authentication
+bearer_auth = None
+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=CLERK_ISSUER,
+ algorithm="RS256",
+ audience=None, # Disable audience validation - Clerk uses different audience format
+ required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
+ )
+ 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,
+ issuer="https://dev.yargimcp.com",
+ 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}")
+
+custom_middleware = [
+ Middleware(
+ CORSMiddleware,
+ allow_origins=cors_origins,
+ allow_credentials=True,
+ allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
+ allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
+ ),
+]
+
+# Create MCP app with Bearer authentication
+mcp_server = create_app(auth=bearer_auth)
+
+# Add Starlette middleware to FastAPI (not MCP)
+# MCP already has Bearer auth, no need for additional middleware on MCP level
+
+# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
+mcp_app = mcp_server.http_app(path="/")
+
+# Configure JSON encoder for proper Turkish character support
+import json
+from fastapi.responses import JSONResponse
+
+class UTF8JSONResponse(JSONResponse):
+ def __init__(self, content=None, status_code=200, headers=None, **kwargs):
+ if headers is None:
+ headers = {}
+ headers["Content-Type"] = "application/json; charset=utf-8"
+ super().__init__(content, status_code, headers, **kwargs)
+
+ def render(self, content) -> bytes:
+ return json.dumps(
+ content,
+ ensure_ascii=False,
+ allow_nan=False,
+ indent=None,
+ separators=(",", ":"),
+ ).encode("utf-8")
+
+# Create FastAPI wrapper application
+app = FastAPI(
+ title="Yargı MCP Server",
+ description="MCP server for Turkish legal databases with OAuth authentication",
+ version="0.1.0",
+ middleware=custom_middleware,
+ default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
+)
+
+# Add Stripe webhook router to FastAPI
+app.include_router(stripe_router, prefix="/api")
+
+# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints)
+app.include_router(mcp_auth_router)
+
+# Custom 401 exception handler for MCP spec compliance
+@app.exception_handler(401)
+async def custom_401_handler(request: Request, exc: HTTPException):
+ """Custom 401 handler that adds WWW-Authenticate header as required by MCP spec"""
+ response = await http_exception_handler(request, exc)
+
+ # Add WWW-Authenticate header pointing to protected resource metadata
+ # as required by RFC 9728 Section 5.1 and MCP Authorization spec
+ response.headers["WWW-Authenticate"] = (
+ 'Bearer '
+ 'error="invalid_token", '
+ 'error_description="The access token is missing or invalid", '
+ f'resource="{BASE_URL}/.well-known/oauth-protected-resource"'
+ )
+
+ return response
+
+# FastAPI health check endpoint - BEFORE mounting MCP app
+@app.get("/health")
+async def health_check():
+ """Health check endpoint for monitoring"""
+ return JSONResponse({
+ "status": "healthy",
+ "service": "Yargı MCP Server",
+ "version": "0.1.0",
+ "tools_count": len(mcp_server._tool_manager._tools),
+ "auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
+ })
+
+# Add explicit redirect for /mcp to /mcp/ with method preservation
+@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"])
+async def redirect_to_slash(request: Request):
+ """Redirect /mcp to /mcp/ preserving HTTP method with 308"""
+ from fastapi.responses import RedirectResponse
+ return RedirectResponse(url="/mcp/", status_code=308)
+
+# Mount MCP app at /mcp/ with trailing slash
+app.mount("/mcp/", mcp_app)
+
+# Set the lifespan context after mounting
+app.router.lifespan_context = mcp_app.lifespan
+
+
+# SSE transport deprecated - removed
+
+# FastAPI root endpoint
+@app.get("/")
+async def root():
+ """Root endpoint with service information"""
+ return JSONResponse({
+ "service": "Yargı MCP Server",
+ "description": "MCP server for Turkish legal databases with OAuth authentication",
+ "endpoints": {
+ "mcp": "/mcp",
+ "health": "/health",
+ "status": "/status",
+ "stripe_webhook": "/api/stripe/webhook",
+ "oauth_login": "/auth/login",
+ "oauth_callback": "/auth/callback",
+ "oauth_google": "/auth/google/login",
+ "user_info": "/auth/user"
+ },
+ "transports": {
+ "http": "/mcp"
+ },
+ "supported_databases": [
+ "Yargıtay (Court of Cassation)",
+ "Danıştay (Council of State)",
+ "Emsal (Precedent)",
+ "Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes)",
+ "Anayasa Mahkemesi (Constitutional Court)",
+ "Kamu İhale Kurulu (Public Procurement Authority)",
+ "Rekabet Kurumu (Competition Authority)",
+ "Sayıştay (Court of Accounts)",
+ "Bedesten API (Multiple courts)"
+ ],
+ "authentication": {
+ "enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true",
+ "type": "OAuth 2.0 via Clerk",
+ "issuer": os.getenv("CLERK_ISSUER", "https://clerk.accounts.dev"),
+ "providers": ["google"],
+ "flow": "authorization_code"
+ }
+ })
+
+# OAuth 2.0 Authorization Server Metadata proxy (for MCP clients that can't reach Clerk directly)
+# MCP Auth Toolkit expects this to be under /mcp/.well-known/oauth-authorization-server
+@app.get("/mcp/.well-known/oauth-authorization-server")
+async def oauth_authorization_server():
+ """OAuth 2.0 Authorization Server Metadata proxy to Clerk - MCP Auth Toolkit standard location"""
+ return JSONResponse({
+ "issuer": BASE_URL,
+ "authorization_endpoint": "https://yargimcp.com/mcp-callback",
+ "token_endpoint": f"{BASE_URL}/token",
+ "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
+ "scopes_supported": ["read", "search", "openid", "profile", "email"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"],
+ "code_challenge_methods_supported": ["S256"],
+ "service_documentation": f"{BASE_URL}/mcp",
+ "registration_endpoint": f"{BASE_URL}/register",
+ "resource_documentation": f"{BASE_URL}/mcp"
+ })
+
+# Claude AI MCP specific endpoint format
+@app.get("/.well-known/oauth-authorization-server/mcp")
+async def oauth_authorization_server_mcp_suffix():
+ """OAuth 2.0 Authorization Server Metadata - Claude AI MCP specific format"""
+ return JSONResponse({
+ "issuer": BASE_URL,
+ "authorization_endpoint": "https://yargimcp.com/mcp-callback",
+ "token_endpoint": f"{BASE_URL}/token",
+ "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
+ "scopes_supported": ["read", "search", "openid", "profile", "email"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"],
+ "code_challenge_methods_supported": ["S256"],
+ "service_documentation": f"{BASE_URL}/mcp",
+ "registration_endpoint": f"{BASE_URL}/register",
+ "resource_documentation": f"{BASE_URL}/mcp"
+ })
+
+@app.get("/.well-known/oauth-protected-resource/mcp")
+async def oauth_protected_resource_mcp_suffix():
+ """OAuth 2.0 Protected Resource Metadata - Claude AI MCP specific format"""
+ return JSONResponse({
+ "resource": BASE_URL,
+ "authorization_servers": [
+ BASE_URL
+ ],
+ "scopes_supported": ["read", "search"],
+ "bearer_methods_supported": ["header"],
+ "resource_documentation": f"{BASE_URL}/mcp",
+ "resource_policy_uri": f"{BASE_URL}/privacy"
+ })
+
+# Keep root level for compatibility with some MCP clients
+@app.get("/.well-known/oauth-authorization-server")
+async def oauth_authorization_server_root():
+ """OAuth 2.0 Authorization Server Metadata proxy to Clerk - root level for compatibility"""
+ return JSONResponse({
+ "issuer": BASE_URL,
+ "authorization_endpoint": "https://yargimcp.com/mcp-callback",
+ "token_endpoint": f"{BASE_URL}/token",
+ "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
+ "scopes_supported": ["read", "search", "openid", "profile", "email"],
+ "subject_types_supported": ["public"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"],
+ "code_challenge_methods_supported": ["S256"],
+ "service_documentation": f"{BASE_URL}/mcp",
+ "registration_endpoint": f"{BASE_URL}/register",
+ "resource_documentation": f"{BASE_URL}/mcp"
+ })
+
+# Note: GET /mcp is handled by the mounted MCP app itself
+# This prevents 405 Method Not Allowed errors on POST requests
+
+# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
+@app.get("/.well-known/oauth-protected-resource")
+async def oauth_protected_resource():
+ """OAuth 2.0 Protected Resource Metadata as required by MCP spec"""
+ return JSONResponse({
+ "resource": BASE_URL,
+ "authorization_servers": [
+ BASE_URL
+ ],
+ "scopes_supported": ["read", "search"],
+ "bearer_methods_supported": ["header"],
+ "resource_documentation": f"{BASE_URL}/mcp",
+ "resource_policy_uri": f"{BASE_URL}/privacy"
+ })
+
+# Standard well-known discovery endpoint
+@app.get("/.well-known/mcp")
+async def well_known_mcp():
+ """Standard MCP discovery endpoint"""
+ return JSONResponse({
+ "mcp_server": {
+ "name": "Yargı MCP Server",
+ "version": "0.1.0",
+ "endpoint": f"{BASE_URL}/mcp",
+ "authentication": {
+ "type": "oauth2",
+ "authorization_url": f"{BASE_URL}/auth/login",
+ "scopes": ["read", "search"]
+ },
+ "capabilities": ["tools", "resources"],
+ "tools_count": len(mcp_server._tool_manager._tools)
+ }
+ })
+
+# MCP Discovery endpoint for ChatGPT integration
+@app.get("/mcp/discovery")
+async def mcp_discovery():
+ """MCP Discovery endpoint for ChatGPT and other MCP clients"""
+ return JSONResponse({
+ "name": "Yargı MCP Server",
+ "description": "MCP server for Turkish legal databases",
+ "version": "0.1.0",
+ "protocol": "mcp",
+ "transport": "http",
+ "endpoint": "/mcp",
+ "authentication": {
+ "type": "oauth2",
+ "authorization_url": "/auth/login",
+ "token_url": "/auth/callback",
+ "scopes": ["read", "search"],
+ "provider": "clerk"
+ },
+ "capabilities": {
+ "tools": True,
+ "resources": True,
+ "prompts": False
+ },
+ "tools_count": len(mcp_server._tool_manager._tools),
+ "contact": {
+ "url": BASE_URL,
+ "email": "support@yargi-mcp.dev"
+ }
+ })
+
+# FastAPI status endpoint
+@app.get("/status")
+async def status():
+ """Status endpoint with detailed information"""
+ tools = []
+ for tool in mcp_server._tool_manager._tools.values():
+ tools.append({
+ "name": tool.name,
+ "description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description
+ })
+
+ return JSONResponse({
+ "status": "operational",
+ "tools": tools,
+ "total_tools": len(tools),
+ "transport": "streamable_http",
+ "architecture": "FastAPI wrapper + MCP Starlette sub-app",
+ "auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled"
+ })
+
+# Note: JWT token validation is now handled entirely by Clerk
+# All authentication flows use Clerk JWT tokens directly
+
+async def validate_clerk_session(request: Request, clerk_token: str = None) -> str:
+ """Validate Clerk session from cookies or JWT token and return user_id"""
+ logger.info(f"Validating Clerk session - token provided: {bool(clerk_token)}")
+
+ try:
+ # Try to import Clerk SDK
+ from clerk_backend_api import Clerk
+ clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
+
+ # Try JWT token first (from URL parameter)
+ if clerk_token:
+ logger.info("Validating Clerk JWT token from URL parameter")
+ try:
+ # Extract session_id from JWT token and verify with Clerk
+ import jwt
+ decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
+ session_id = decoded_token.get("sid") # Use standard JWT 'sid' claim
+
+ if session_id:
+ # Verify with Clerk using session_id
+ session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
+ user_id = session.user_id if session else None
+
+ if user_id:
+ logger.info(f"JWT token validation successful - user_id: {user_id}")
+ return user_id
+ else:
+ logger.error("JWT token validation failed - no user_id in session")
+ else:
+ logger.error("No session_id found in JWT token")
+ except Exception as e:
+ logger.error(f"JWT token validation failed: {str(e)}")
+ # Fall through to cookie validation
+
+ # Fallback to cookie validation
+ logger.info("Attempting cookie-based session validation")
+ 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"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")
+ return "dev_user_123"
+ except Exception as e:
+ logger.error(f"Session validation failed: {str(e)}")
+ raise HTTPException(status_code=401, detail=f"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(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"""
+
+
+ MCP Connection Successful
+
+
+
+ ✅ MCP Connection Successful!
+ Your Yargı MCP integration is now active.
+
+ Authentication:
+ Use your Clerk JWT token directly with Bearer authentication
+
+ You can now close this window and return to your MCP client.
+
+
+
+ """)
+
+ except HTTPException as e:
+ logger.error(f"MCP OAuth callback failed: {e.detail}")
+ return HTMLResponse(f"""
+
+
+ MCP Connection Failed
+
+
+
+ ❌ MCP Connection Failed
+ {e.detail}
+
+ Debug Info:
+ Clerk Token: {'✅ Provided' if clerk_token else '❌ Missing'}
+ Error: {e.detail}
+ Status: {e.status_code}
+
+ Please try again or contact support.
+ Return to Sign In
+
+
+ """, status_code=e.status_code)
+ except Exception as e:
+ logger.error(f"Unexpected error in MCP OAuth callback: {str(e)}")
+ return HTMLResponse(f"""
+
+
+ MCP Connection Error
+
+
+
+ ❌ Unexpected Error
+ An unexpected error occurred during authentication.
+ Error: {str(e)}
+ Return to Sign In
+
+
+ """, status_code=500)
+
+# OAuth2 Token Endpoint - Now uses Clerk JWT tokens directly
+@app.post("/auth/mcp-token")
+async def mcp_token_endpoint(request: Request):
+ """OAuth2 token endpoint for MCP clients - returns Clerk JWT token info"""
+ try:
+ # Validate Clerk session
+ user_id = await validate_clerk_session(request)
+
+ return JSONResponse({
+ "message": "Use your Clerk JWT token directly with Bearer authentication",
+ "token_type": "Bearer",
+ "scope": "yargi.read",
+ "user_id": user_id,
+ "instructions": "Include 'Authorization: Bearer YOUR_CLERK_JWT_TOKEN' in your requests"
+ })
+ except HTTPException as e:
+ return JSONResponse(
+ status_code=e.status_code,
+ content={"error": "invalid_request", "error_description": e.detail}
+ )
+
+# Note: Only HTTP transport supported - SSE transport deprecated
+
+# Export for uvicorn
+__all__ = ["app"]
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py
new file mode 100644
index 0000000..7a2195d
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py
@@ -0,0 +1,17 @@
+# bddk_mcp_module/__init__.py
+
+from .client import BddkApiClient
+from .models import (
+ BddkSearchRequest,
+ BddkDecisionSummary,
+ BddkSearchResult,
+ BddkDocumentMarkdown
+)
+
+__all__ = [
+ "BddkApiClient",
+ "BddkSearchRequest",
+ "BddkDecisionSummary",
+ "BddkSearchResult",
+ "BddkDocumentMarkdown"
+]
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py
new file mode 100644
index 0000000..158e082
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py
@@ -0,0 +1,247 @@
+# bddk_mcp_module/client.py
+
+import httpx
+from typing import List, Optional, Dict, Any
+import logging
+import os
+import re
+import io
+import math
+from urllib.parse import urlparse
+from markitdown import MarkItDown
+
+from .models import (
+ BddkSearchRequest,
+ BddkDecisionSummary,
+ BddkSearchResult,
+ BddkDocumentMarkdown
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+
+class BddkApiClient:
+ """
+ API client for searching and retrieving BDDK (Banking Regulation Authority) decisions
+ using Tavily Search API for discovery and direct HTTP requests for content retrieval.
+ """
+
+ TAVILY_API_URL = "https://api.tavily.com/search"
+ BDDK_BASE_URL = "https://www.bddk.org.tr"
+ DOCUMENT_URL_TEMPLATE = "https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}"
+ DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page
+
+ def __init__(self, request_timeout: float = 60.0):
+ """Initialize the BDDK API client."""
+ self.tavily_api_key = os.getenv("TAVILY_API_KEY")
+ if not self.tavily_api_key:
+ # Fallback to development token
+ self.tavily_api_key = "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"
+ logger.info("Using fallback Tavily API token (development token)")
+ else:
+ logger.info("Using Tavily API key from environment variable")
+
+ self.http_client = httpx.AsyncClient(
+ headers={
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
+ },
+ timeout=httpx.Timeout(request_timeout)
+ )
+ self.markitdown = MarkItDown()
+
+ async def close_client_session(self):
+ """Close the HTTP client session."""
+ await self.http_client.aclose()
+ logger.info("BddkApiClient: HTTP client session closed.")
+
+ def _extract_document_id(self, url: str) -> Optional[str]:
+ """Extract document ID from BDDK URL."""
+ # Primary pattern: https://www.bddk.org.tr/Mevzuat/DokumanGetir/310
+ match = re.search(r'/DokumanGetir/(\d+)', url)
+ if match:
+ return match.group(1)
+
+ # Alternative patterns for different BDDK URL formats
+ # Pattern: /Liste/55 -> use as document ID
+ match = re.search(r'/Liste/(\d+)', url)
+ if match:
+ return match.group(1)
+
+ # Pattern: /EkGetir/13?ekId=381 -> use ekId as document ID
+ match = re.search(r'ekId=(\d+)', url)
+ if match:
+ return match.group(1)
+
+ return None
+
+ async def search_decisions(
+ self,
+ request: BddkSearchRequest
+ ) -> BddkSearchResult:
+ """
+ Search for BDDK decisions using Tavily API.
+
+ Args:
+ request: Search request parameters
+
+ Returns:
+ BddkSearchResult with matching decisions
+ """
+ try:
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.tavily_api_key}"
+ }
+
+ # Tavily API request - enhanced for BDDK decision documents
+ query = f"{request.keywords} \"Karar Sayısı\""
+ payload = {
+ "query": query,
+ "country": "turkey",
+ "include_domains": ["https://www.bddk.org.tr/Mevzuat/DokumanGetir"],
+ "max_results": request.pageSize,
+ "search_depth": "advanced"
+ }
+
+ # Calculate offset for pagination
+ if request.page > 1:
+ # Tavily doesn't have direct pagination, so we'll need to handle this
+ # For now, we'll just return empty for pages > 1
+ logger.warning(f"Tavily API doesn't support pagination. Page {request.page} requested.")
+
+ response = await self.http_client.post(
+ self.TAVILY_API_URL,
+ json=payload,
+ headers=headers
+ )
+ response.raise_for_status()
+
+ data = response.json()
+
+ # Log raw Tavily response for debugging
+ logger.info(f"Tavily returned {len(data.get('results', []))} results")
+
+ # Convert Tavily results to our format
+ decisions = []
+ for result in data.get("results", []):
+ # Extract document ID from URL
+ url = result.get("url", "")
+ logger.debug(f"Processing URL: {url}")
+ doc_id = self._extract_document_id(url)
+ if doc_id:
+ decision = BddkDecisionSummary(
+ title=result.get("title", "").replace("[PDF] ", "").strip(),
+ document_id=doc_id,
+ content=result.get("content", "")[:500] # Limit content length
+ )
+ decisions.append(decision)
+ logger.debug(f"Added decision: {decision.title} (ID: {doc_id})")
+ else:
+ logger.warning(f"Could not extract document ID from URL: {url}")
+
+ return BddkSearchResult(
+ decisions=decisions,
+ total_results=len(data.get("results", [])),
+ page=request.page,
+ pageSize=request.pageSize
+ )
+
+ except httpx.HTTPStatusError as e:
+ logger.error(f"HTTP error searching BDDK decisions: {e}")
+ if e.response.status_code == 401:
+ raise Exception("Tavily API authentication failed. Check API key.")
+ raise Exception(f"Failed to search BDDK decisions: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error searching BDDK decisions: {e}")
+ raise Exception(f"Failed to search BDDK decisions: {str(e)}")
+
+ async def get_document_markdown(
+ self,
+ document_id: str,
+ page_number: int = 1
+ ) -> BddkDocumentMarkdown:
+ """
+ Retrieve a BDDK document and convert it to Markdown format.
+
+ Args:
+ document_id: BDDK document ID (e.g., '310')
+ page_number: Page number for paginated content (1-indexed)
+
+ Returns:
+ BddkDocumentMarkdown with paginated content
+ """
+ try:
+ # Try different URL patterns for BDDK documents
+ potential_urls = [
+ f"https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}",
+ f"https://www.bddk.org.tr/Mevzuat/Liste/{document_id}",
+ f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/13?ekId={document_id}",
+ f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/5?ekId={document_id}"
+ ]
+
+ document_url = None
+ response = None
+
+ # Try each URL pattern until one works
+ for url in potential_urls:
+ try:
+ logger.info(f"Trying BDDK document URL: {url}")
+ response = await self.http_client.get(
+ url,
+ follow_redirects=True
+ )
+ response.raise_for_status()
+ document_url = url
+ break
+ except httpx.HTTPStatusError:
+ continue
+
+ if not response or not document_url:
+ raise Exception(f"Could not find document with ID {document_id}")
+
+ logger.info(f"Successfully fetched BDDK document from: {document_url}")
+
+ # Determine content type
+ content_type = response.headers.get("content-type", "").lower()
+
+ # Convert to Markdown based on content type
+ if "pdf" in content_type:
+ # Handle PDF documents
+ pdf_stream = io.BytesIO(response.content)
+ result = self.markitdown.convert_stream(pdf_stream, file_extension=".pdf")
+ markdown_content = result.text_content
+ else:
+ # Handle HTML documents
+ html_stream = io.BytesIO(response.content)
+ result = self.markitdown.convert_stream(html_stream, file_extension=".html")
+ markdown_content = result.text_content
+
+ # Clean up the markdown content
+ markdown_content = markdown_content.strip()
+
+ # Calculate pagination
+ total_length = len(markdown_content)
+ total_pages = math.ceil(total_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE)
+
+ # Extract the requested page
+ start_idx = (page_number - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ end_idx = start_idx + self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ page_content = markdown_content[start_idx:end_idx]
+
+ return BddkDocumentMarkdown(
+ document_id=document_id,
+ markdown_content=page_content,
+ page_number=page_number,
+ total_pages=total_pages
+ )
+
+ except httpx.HTTPStatusError as e:
+ logger.error(f"HTTP error fetching BDDK document {document_id}: {e}")
+ raise Exception(f"Failed to fetch BDDK document: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error processing BDDK document {document_id}: {e}")
+ raise Exception(f"Failed to process BDDK document: {str(e)}")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py
new file mode 100644
index 0000000..c945f26
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py
@@ -0,0 +1,43 @@
+# bddk_mcp_module/models.py
+
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+class BddkSearchRequest(BaseModel):
+ """
+ Request model for searching BDDK decisions via Tavily API.
+
+ BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) is Turkey's Banking
+ Regulation and Supervision Agency responsible for banking licenses,
+ electronic money institutions, and financial regulations.
+ """
+ keywords: str = Field(..., description="Search keywords in Turkish")
+ page: int = Field(1, ge=1, description="Page number (1-indexed)")
+ pageSize: int = Field(10, ge=1, le=50, description="Results per page (1-50)")
+
+class BddkDecisionSummary(BaseModel):
+ """Summary of a BDDK decision from search results."""
+ title: str = Field(..., description="Decision title")
+ document_id: str = Field(..., description="BDDK document ID (e.g., '310')")
+ content: str = Field(..., description="Decision summary/excerpt")
+
+class BddkSearchResult(BaseModel):
+ """Response model for BDDK decision search results."""
+ decisions: List[BddkDecisionSummary] = Field(
+ default_factory=list,
+ description="List of matching BDDK decisions"
+ )
+ total_results: int = Field(0, description="Total number of results")
+ page: int = Field(1, description="Current page number")
+ pageSize: int = Field(10, description="Results per page")
+
+class BddkDocumentMarkdown(BaseModel):
+ """
+ BDDK decision document converted to Markdown format.
+
+ Supports paginated content for long documents (5000 chars per page).
+ """
+ document_id: str = Field(..., description="BDDK document ID")
+ markdown_content: str = Field("", description="Document content in Markdown")
+ page_number: int = Field(1, description="Current page number")
+ total_pages: int = Field(1, description="Total number of pages")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py
new file mode 100644
index 0000000..90b95d3
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py
@@ -0,0 +1 @@
+# bedesten_mcp_module/__init__.py
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py
new file mode 100644
index 0000000..ad6f1a8
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py
@@ -0,0 +1,181 @@
+# bedesten_mcp_module/client.py
+
+import httpx
+import base64
+from typing import Optional
+import logging
+from markitdown import MarkItDown
+import io
+
+from .models import (
+ BedestenSearchRequest, BedestenSearchResponse,
+ BedestenDocumentRequest, BedestenDocumentResponse,
+ BedestenDocumentMarkdown, BedestenDocumentRequestData
+)
+from .enums import get_full_birim_adi
+
+logger = logging.getLogger(__name__)
+
+class BedestenApiClient:
+ """
+ API Client for Bedesten (bedesten.adalet.gov.tr) - Alternative legal decision search system.
+ Currently used for Yargıtay decisions, but can be extended for other court types.
+ """
+ BASE_URL = "https://bedesten.adalet.gov.tr"
+ SEARCH_ENDPOINT = "/emsal-karar/searchDocuments"
+ DOCUMENT_ENDPOINT = "/emsal-karar/getDocumentContent"
+
+ def __init__(self, request_timeout: float = 60.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Accept": "*/*",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "AdaletApplicationName": "UyapMevzuat",
+ "Content-Type": "application/json; charset=utf-8",
+ "Origin": "https://mevzuat.adalet.gov.tr",
+ "Referer": "https://mevzuat.adalet.gov.tr/",
+ "Sec-Fetch-Dest": "empty",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Site": "same-site",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
+ },
+ timeout=request_timeout
+ )
+
+ async def search_documents(self, search_request: BedestenSearchRequest) -> BedestenSearchResponse:
+ """
+ Search for documents using Bedesten API.
+ Currently supports: YARGITAYKARARI, DANISTAYKARARI, YERELHUKMAHKARARI, etc.
+ """
+ logger.info(f"BedestenApiClient: Searching documents with phrase: {search_request.data.phrase}")
+
+ # Map abbreviated birimAdi to full Turkish name before sending to API
+ original_birim_adi = search_request.data.birimAdi
+ mapped_birim_adi = get_full_birim_adi(original_birim_adi)
+ search_request.data.birimAdi = mapped_birim_adi
+ if original_birim_adi != "ALL":
+ logger.info(f"BedestenApiClient: Mapped birimAdi '{original_birim_adi}' to '{mapped_birim_adi}'")
+
+ try:
+ # Create request dict and remove birimAdi if empty
+ request_dict = search_request.model_dump()
+ if not request_dict["data"]["birimAdi"]: # Remove if empty string
+ del request_dict["data"]["birimAdi"]
+
+ response = await self.http_client.post(
+ self.SEARCH_ENDPOINT,
+ json=request_dict
+ )
+ response.raise_for_status()
+ response_json = response.json()
+
+ # Parse and return the response
+ return BedestenSearchResponse(**response_json)
+
+ except httpx.RequestError as e:
+ logger.error(f"BedestenApiClient: HTTP request error during search: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"BedestenApiClient: Error processing search response: {e}")
+ raise
+
+ async def get_document_as_markdown(self, document_id: str) -> BedestenDocumentMarkdown:
+ """
+ Get document content and convert to markdown.
+ Handles both HTML (text/html) and PDF (application/pdf) content types.
+ """
+ logger.info(f"BedestenApiClient: Fetching document for markdown conversion (ID: {document_id})")
+
+ try:
+ # Prepare request
+ doc_request = BedestenDocumentRequest(
+ data=BedestenDocumentRequestData(documentId=document_id)
+ )
+
+ # Get document
+ response = await self.http_client.post(
+ self.DOCUMENT_ENDPOINT,
+ json=doc_request.model_dump()
+ )
+ response.raise_for_status()
+ response_json = response.json()
+ doc_response = BedestenDocumentResponse(**response_json)
+
+ # Decode base64 content
+ content_bytes = base64.b64decode(doc_response.data.content)
+ mime_type = doc_response.data.mimeType
+
+ logger.info(f"BedestenApiClient: Document mime type: {mime_type}")
+
+ # Convert to markdown based on mime type
+ if mime_type == "text/html":
+ html_content = content_bytes.decode('utf-8')
+ markdown_content = self._convert_html_to_markdown(html_content)
+ elif mime_type == "application/pdf":
+ markdown_content = self._convert_pdf_to_markdown(content_bytes)
+ else:
+ logger.warning(f"Unsupported mime type: {mime_type}")
+ markdown_content = f"Unsupported content type: {mime_type}. Unable to convert to markdown."
+
+ return BedestenDocumentMarkdown(
+ documentId=document_id,
+ markdown_content=markdown_content,
+ source_url=f"{self.BASE_URL}/document/{document_id}",
+ mime_type=mime_type
+ )
+
+ except httpx.RequestError as e:
+ logger.error(f"BedestenApiClient: HTTP error fetching document {document_id}: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"BedestenApiClient: Error processing document {document_id}: {e}")
+ raise
+
+ def _convert_html_to_markdown(self, html_content: str) -> Optional[str]:
+ """Convert HTML to Markdown using MarkItDown"""
+ if not html_content:
+ return None
+
+ try:
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_content.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ result = md_converter.convert(html_stream)
+ markdown_content = result.text_content
+
+ logger.info("Successfully converted HTML to Markdown")
+ return markdown_content
+
+ except Exception as e:
+ logger.error(f"Error converting HTML to Markdown: {e}")
+ return f"Error converting HTML content: {str(e)}"
+
+ def _convert_pdf_to_markdown(self, pdf_bytes: bytes) -> Optional[str]:
+ """Convert PDF to Markdown using MarkItDown"""
+ if not pdf_bytes:
+ return None
+
+ try:
+ # Create BytesIO stream from PDF bytes
+ pdf_stream = io.BytesIO(pdf_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ result = md_converter.convert(pdf_stream)
+ markdown_content = result.text_content
+
+ logger.info("Successfully converted PDF to Markdown")
+ return markdown_content
+
+ except Exception as e:
+ logger.error(f"Error converting PDF to Markdown: {e}")
+ return f"Error converting PDF content: {str(e)}. The document may be corrupted or in an unsupported format."
+
+ async def close_client_session(self):
+ """Close HTTP client session"""
+ await self.http_client.aclose()
+ logger.info("BedestenApiClient: HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py
new file mode 100644
index 0000000..7cef054
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py
@@ -0,0 +1,113 @@
+# bedesten_mcp_module/enums.py
+
+from typing import Literal
+
+# Unified compressed enum for both Yargıtay and Danıştay chambers
+BirimAdiEnum = Literal[
+ "ALL", # All chambers
+
+ # Yargıtay (Court of Cassation) - Civil Chambers
+ "H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10",
+ "H11", "H12", "H13", "H14", "H15", "H16", "H17", "H18", "H19", "H20",
+ "H21", "H22", "H23",
+
+ # Yargıtay - Criminal Chambers
+ "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10",
+ "C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C20",
+ "C21", "C22", "C23",
+
+ # Yargıtay - Councils and Assemblies
+ "HGK", # Hukuk Genel Kurulu
+ "CGK", # Ceza Genel Kurulu
+ "BGK", # Büyük Genel Kurulu
+ "HBK", # Hukuk Daireleri Başkanlar Kurulu
+ "CBK", # Ceza Daireleri Başkanlar Kurulu
+
+ # Danıştay (Council of State) - Chambers
+ "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10",
+ "D11", "D12", "D13", "D14", "D15", "D16", "D17",
+
+ # Danıştay - Councils and Boards
+ "DBGK", # Büyük Gen.Kur. (Grand General Assembly)
+ "IDDK", # İdare Dava Daireleri Kurulu
+ "VDDK", # Vergi Dava Daireleri Kurulu
+ "IBK", # İçtihatları Birleştirme Kurulu
+ "IIK", # İdari İşler Kurulu
+ "DBK", # Başkanlar Kurulu
+
+ # Military High Administrative Court
+ "AYIM", # Askeri Yüksek İdare Mahkemesi
+ "AYIMDK", # Askeri Yüksek İdare Mahkemesi Daireler Kurulu
+ "AYIMB", # Askeri Yüksek İdare Mahkemesi Başsavcılığı
+ "AYIM1", # Askeri Yüksek İdare Mahkemesi 1. Daire
+ "AYIM2", # Askeri Yüksek İdare Mahkemesi 2. Daire
+ "AYIM3" # Askeri Yüksek İdare Mahkemesi 3. Daire
+]
+
+# Mapping from abbreviated values to full Turkish API values
+BIRIM_ADI_MAPPING = {
+ "ALL": None, # Will be handled specially in client
+
+ # Yargıtay Civil Chambers (1-23)
+ "H1": "1. Hukuk Dairesi", "H2": "2. Hukuk Dairesi", "H3": "3. Hukuk Dairesi",
+ "H4": "4. Hukuk Dairesi", "H5": "5. Hukuk Dairesi", "H6": "6. Hukuk Dairesi",
+ "H7": "7. Hukuk Dairesi", "H8": "8. Hukuk Dairesi", "H9": "9. Hukuk Dairesi",
+ "H10": "10. Hukuk Dairesi", "H11": "11. Hukuk Dairesi", "H12": "12. Hukuk Dairesi",
+ "H13": "13. Hukuk Dairesi", "H14": "14. Hukuk Dairesi", "H15": "15. Hukuk Dairesi",
+ "H16": "16. Hukuk Dairesi", "H17": "17. Hukuk Dairesi", "H18": "18. Hukuk Dairesi",
+ "H19": "19. Hukuk Dairesi", "H20": "20. Hukuk Dairesi", "H21": "21. Hukuk Dairesi",
+ "H22": "22. Hukuk Dairesi", "H23": "23. Hukuk Dairesi",
+
+ # Yargıtay Criminal Chambers (1-23)
+ "C1": "1. Ceza Dairesi", "C2": "2. Ceza Dairesi", "C3": "3. Ceza Dairesi",
+ "C4": "4. Ceza Dairesi", "C5": "5. Ceza Dairesi", "C6": "6. Ceza Dairesi",
+ "C7": "7. Ceza Dairesi", "C8": "8. Ceza Dairesi", "C9": "9. Ceza Dairesi",
+ "C10": "10. Ceza Dairesi", "C11": "11. Ceza Dairesi", "C12": "12. Ceza Dairesi",
+ "C13": "13. Ceza Dairesi", "C14": "14. Ceza Dairesi", "C15": "15. Ceza Dairesi",
+ "C16": "16. Ceza Dairesi", "C17": "17. Ceza Dairesi", "C18": "18. Ceza Dairesi",
+ "C19": "19. Ceza Dairesi", "C20": "20. Ceza Dairesi", "C21": "21. Ceza Dairesi",
+ "C22": "22. Ceza Dairesi", "C23": "23. Ceza Dairesi",
+
+ # Yargıtay Councils and Assemblies
+ "HGK": "Hukuk Genel Kurulu",
+ "CGK": "Ceza Genel Kurulu",
+ "BGK": "Büyük Genel Kurulu",
+ "HBK": "Hukuk Daireleri Başkanlar Kurulu",
+ "CBK": "Ceza Daireleri Başkanlar Kurulu",
+
+ # Danıştay Chambers (1-17)
+ "D1": "1. Daire", "D2": "2. Daire", "D3": "3. Daire", "D4": "4. Daire",
+ "D5": "5. Daire", "D6": "6. Daire", "D7": "7. Daire", "D8": "8. Daire",
+ "D9": "9. Daire", "D10": "10. Daire", "D11": "11. Daire", "D12": "12. Daire",
+ "D13": "13. Daire", "D14": "14. Daire", "D15": "15. Daire", "D16": "16. Daire",
+ "D17": "17. Daire",
+
+ # Danıştay Councils and Boards
+ "DBGK": "Büyük Gen.Kur.",
+ "IDDK": "İdare Dava Daireleri Kurulu",
+ "VDDK": "Vergi Dava Daireleri Kurulu",
+ "IBK": "İçtihatları Birleştirme Kurulu",
+ "IIK": "İdari İşler Kurulu",
+ "DBK": "Başkanlar Kurulu",
+
+ # Military High Administrative Court
+ "AYIM": "Askeri Yüksek İdare Mahkemesi",
+ "AYIMDK": "Askeri Yüksek İdare Mahkemesi Daireler Kurulu",
+ "AYIMB": "Askeri Yüksek İdare Mahkemesi Başsavcılığı",
+ "AYIM1": "Askeri Yüksek İdare Mahkemesi 1. Daire",
+ "AYIM2": "Askeri Yüksek İdare Mahkemesi 2. Daire",
+ "AYIM3": "Askeri Yüksek İdare Mahkemesi 3. Daire"
+}
+
+# Helper function to get full Turkish name from abbreviated value
+def get_full_birim_adi(abbreviated_value: str) -> str:
+ """Convert abbreviated birimAdi value to full Turkish name for API calls."""
+ if abbreviated_value == "ALL" or not abbreviated_value:
+ return "" # Empty string for ALL or None
+
+ return BIRIM_ADI_MAPPING.get(abbreviated_value, abbreviated_value)
+
+# Helper function to validate abbreviated value
+def is_valid_birim_adi(abbreviated_value: str) -> bool:
+ """Check if abbreviated birimAdi value is valid."""
+ return abbreviated_value in BIRIM_ADI_MAPPING
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py
new file mode 100644
index 0000000..6c6c24d
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py
@@ -0,0 +1,91 @@
+# bedesten_mcp_module/models.py
+
+from pydantic import BaseModel, Field
+from typing import List, Optional, Dict, Any, Literal, Union
+from datetime import datetime
+
+# Import compressed BirimAdiEnum for chamber filtering
+from .enums import BirimAdiEnum
+
+# Court Type Options for Unified Search
+BedestenCourtTypeEnum = Literal[
+ "YARGITAYKARARI", # Yargıtay (Court of Cassation)
+ "DANISTAYKARAR", # Danıştay (Council of State)
+ "YERELHUKUK", # Local Civil Courts
+ "ISTINAFHUKUK", # Civil Courts of Appeals
+ "KYB" # Extraordinary Appeals (Kanun Yararına Bozma)
+]
+
+# Search Request Models
+class BedestenSearchData(BaseModel):
+ pageSize: int = Field(..., description="Results per page (1-10)")
+ pageNumber: int = Field(..., description="Page number (1-indexed)")
+ itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
+ phrase: str = Field(..., description="Search phrase. Supports: 'word', \"exact phrase\", +required, -exclude, AND/OR/NOT operators. No wildcards or regex.")
+ birimAdi: BirimAdiEnum = Field("ALL", description="""
+ Chamber filter (optional). Abbreviated values with Turkish names:
+ • Yargıtay: H1-H23 (1-23. Hukuk Dairesi), C1-C23 (1-23. Ceza Dairesi), HGK (Hukuk Genel Kurulu), CGK (Ceza Genel Kurulu), BGK (Büyük Genel Kurulu), HBK (Hukuk Daireleri Başkanlar Kurulu), CBK (Ceza Daireleri Başkanlar Kurulu)
+ • Danıştay: D1-D17 (1-17. Daire), DBGK (Büyük Gen.Kur.), IDDK (İdare Dava Daireleri Kurulu), VDDK (Vergi Dava Daireleri Kurulu), IBK (İçtihatları Birleştirme Kurulu), IIK (İdari İşler Kurulu), DBK (Başkanlar Kurulu), AYIM (Askeri Yüksek İdare Mahkemesi), AYIM1-3 (Askeri Yüksek İdare Mahkemesi 1-3. Daire)
+ """)
+ kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)")
+ kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)")
+ sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="Sort fields")
+ sortDirection: str = Field(default="desc", description="Sort direction (asc/desc)")
+
+class BedestenSearchRequest(BaseModel):
+ data: BedestenSearchData
+ applicationName: str = "UyapMevzuat"
+ paging: bool = True
+
+# Search Response Models
+class BedestenItemType(BaseModel):
+ name: str
+ description: str
+
+class BedestenDecisionEntry(BaseModel):
+ documentId: str
+ itemType: BedestenItemType
+ birimId: Optional[str] = None
+ birimAdi: Optional[str]
+ esasNoYil: Optional[int] = None
+ esasNoSira: Optional[int] = None
+ kararNoYil: Optional[int] = None
+ kararNoSira: Optional[int] = None
+ kararTuru: Optional[str] = None
+ kararTarihi: str
+ kararTarihiStr: str
+ kesinlesmeDurumu: Optional[str] = None
+ kararNo: Optional[str] = None
+ esasNo: Optional[str] = None
+
+class BedestenSearchDataResponse(BaseModel):
+ emsalKararList: List[BedestenDecisionEntry]
+ total: int
+ start: int
+
+class BedestenSearchResponse(BaseModel):
+ data: Optional[BedestenSearchDataResponse]
+ metadata: Dict[str, Any]
+
+# Document Request/Response Models
+class BedestenDocumentRequestData(BaseModel):
+ documentId: str
+
+class BedestenDocumentRequest(BaseModel):
+ data: BedestenDocumentRequestData
+ applicationName: str = "UyapMevzuat"
+
+class BedestenDocumentData(BaseModel):
+ content: str # Base64 encoded HTML or PDF
+ mimeType: str
+ version: int
+
+class BedestenDocumentResponse(BaseModel):
+ data: BedestenDocumentData
+ metadata: Dict[str, Any]
+
+class BedestenDocumentMarkdown(BaseModel):
+ documentId: str = Field(..., description="The document ID (Belge Kimliği) from Bedesten")
+ markdown_content: Optional[str] = Field(None, description="The decision content (Karar İçeriği) converted to Markdown")
+ source_url: str = Field(..., description="The source URL (Kaynak URL) of the document")
+ mime_type: Optional[str] = Field(None, description="Original content type (İçerik Türü) (text/html or application/pdf)")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/check_response_format.py b/saidsurucu-yargi-mcp-f5fa007/check_response_format.py
new file mode 100644
index 0000000..1e814c4
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/check_response_format.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+from fastmcp import Client
+from mcp_server_main import app
+import json
+import asyncio
+
+async def check_response_format():
+ client = Client(app)
+ async with client:
+ result = await client.call_tool('search_bedesten_unified', {
+ 'phrase': 'mülkiyet',
+ 'court_types': ['YARGITAYKARARI'],
+ 'birimAdi': 'H1',
+ 'pageSize': 3
+ })
+ if result and result.content:
+ data = json.loads(result.content[0].text)
+ print('Response keys:', list(data.keys()))
+ print('Sample response:', json.dumps(data, indent=2, ensure_ascii=False)[:500])
+
+asyncio.run(check_response_format())
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py
new file mode 100644
index 0000000..0a713ff
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py
@@ -0,0 +1,192 @@
+# danistay_mcp_module/client.py
+
+import httpx
+from bs4 import BeautifulSoup
+from typing import Dict, Any, List, Optional
+import logging
+import html
+import re
+import io
+from markitdown import MarkItDown
+
+from .models import (
+ DanistayKeywordSearchRequest,
+ DanistayDetailedSearchRequest,
+ DanistayApiResponse,
+ DanistayDocumentMarkdown,
+ DanistayKeywordSearchRequestData,
+ DanistayDetailedSearchRequestData
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+class DanistayApiClient:
+ BASE_URL = "https://karararama.danistay.gov.tr"
+ KEYWORD_SEARCH_ENDPOINT = "/aramalist"
+ DETAILED_SEARCH_ENDPOINT = "/aramadetaylist"
+ DOCUMENT_ENDPOINT = "/getDokuman"
+
+ def __init__(self, request_timeout: float = 30.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Content-Type": "application/json; charset=UTF-8", # Arama endpoint'leri için
+ "Accept": "application/json, text/plain, */*", # Arama endpoint'leri için
+ "X-Requested-With": "XMLHttpRequest",
+ },
+ timeout=request_timeout,
+ verify=False
+ )
+
+ def _prepare_keywords_for_api(self, keywords: List[str]) -> List[str]:
+ return ['"' + k.strip('"') + '"' for k in keywords if k and k.strip()]
+
+ async def search_keyword_decisions(
+ self,
+ params: DanistayKeywordSearchRequest
+ ) -> DanistayApiResponse:
+ data_for_payload = DanistayKeywordSearchRequestData(
+ andKelimeler=self._prepare_keywords_for_api(params.andKelimeler),
+ orKelimeler=self._prepare_keywords_for_api(params.orKelimeler),
+ notAndKelimeler=self._prepare_keywords_for_api(params.notAndKelimeler),
+ notOrKelimeler=self._prepare_keywords_for_api(params.notOrKelimeler),
+ pageSize=params.pageSize,
+ pageNumber=params.pageNumber
+ )
+ final_payload = {"data": data_for_payload.model_dump(exclude_none=True)}
+ logger.info(f"DanistayApiClient: Performing KEYWORD search via {self.KEYWORD_SEARCH_ENDPOINT} with payload: {final_payload}")
+ return await self._execute_api_search(self.KEYWORD_SEARCH_ENDPOINT, final_payload)
+
+ async def search_detailed_decisions(
+ self,
+ params: DanistayDetailedSearchRequest
+ ) -> DanistayApiResponse:
+ data_for_payload = DanistayDetailedSearchRequestData(
+ daire=params.daire or "",
+ esasYil=params.esasYil or "",
+ esasIlkSiraNo=params.esasIlkSiraNo or "",
+ esasSonSiraNo=params.esasSonSiraNo or "",
+ kararYil=params.kararYil or "",
+ kararIlkSiraNo=params.kararIlkSiraNo or "",
+ kararSonSiraNo=params.kararSonSiraNo or "",
+ baslangicTarihi=params.baslangicTarihi or "",
+ bitisTarihi=params.bitisTarihi or "",
+ mevzuatNumarasi=params.mevzuatNumarasi or "",
+ mevzuatAdi=params.mevzuatAdi or "",
+ madde=params.madde or "",
+ siralama="1",
+ siralamaDirection="desc",
+ pageSize=params.pageSize,
+ pageNumber=params.pageNumber
+ )
+ # Create request dict and remove empty string fields to avoid API issues
+ payload_dict = data_for_payload.model_dump(exclude_defaults=False, exclude_none=False)
+ # Remove empty string fields that might cause API issues
+ cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""}
+ final_payload = {"data": cleaned_payload}
+ logger.info(f"DanistayApiClient: Performing DETAILED search via {self.DETAILED_SEARCH_ENDPOINT} with payload: {final_payload}")
+ return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
+
+ async def _execute_api_search(self, endpoint: str, payload: Dict) -> DanistayApiResponse:
+ try:
+ response = await self.http_client.post(endpoint, json=payload)
+ response.raise_for_status()
+ response_json_data = response.json()
+ logger.debug(f"DanistayApiClient: Raw API response from {endpoint}: {response_json_data}")
+ api_response_parsed = DanistayApiResponse(**response_json_data)
+ if api_response_parsed.data and api_response_parsed.data.data:
+ for decision_item in api_response_parsed.data.data:
+ if decision_item.id:
+ decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}"
+ return api_response_parsed
+ except httpx.RequestError as e:
+ logger.error(f"DanistayApiClient: HTTP request error during search to {endpoint}: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"DanistayApiClient: Error processing or validating search response from {endpoint}: {e}")
+ raise
+
+ def _convert_html_to_markdown_danistay(self, direct_html_content: str) -> Optional[str]:
+ """
+ Converts direct HTML content (assumed from Danıştay /getDokuman) to Markdown.
+ """
+ if not direct_html_content:
+ return None
+
+ # Basic HTML unescaping and fixing common escaped characters
+ # This step might be less critical if MarkItDown handles them, but good for pre-cleaning.
+ processed_html = html.unescape(direct_html_content)
+ processed_html = processed_html.replace('\\"', '"') # If any such JS-escaped strings exist
+ # Danistay HTML doesn't seem to have \\r\\n etc. from the example, but keeping for robustness
+ processed_html = processed_html.replace('\\r\\n', '\n').replace('\\n', '\n').replace('\\t', '\t')
+
+ # For simplicity and to leverage MarkItDown's capability to handle full docs,
+ # we pass the pre-processed full HTML.
+ html_input_for_markdown = processed_html
+
+ markdown_text = None
+ try:
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_input_for_markdown.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ conversion_result = md_converter.convert(html_stream)
+ markdown_text = conversion_result.text_content
+ logger.info("DanistayApiClient: HTML to Markdown conversion successful.")
+ except Exception as e:
+ logger.error(f"DanistayApiClient: Error during MarkItDown HTML to Markdown conversion: {e}")
+
+ return markdown_text
+
+ async def get_decision_document_as_markdown(self, id: str) -> DanistayDocumentMarkdown:
+ """
+ Retrieves a specific Danıştay decision by ID and returns its content as Markdown.
+ The /getDokuman endpoint for Danıştay requires arananKelime parameter.
+ """
+ # Add required arananKelime parameter - using empty string as minimum requirement
+ document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={id}&arananKelime="
+ source_url = f"{self.BASE_URL}{document_api_url}"
+ logger.info(f"DanistayApiClient: Fetching Danistay document for Markdown (ID: {id}) from {source_url}")
+
+ try:
+ # For direct HTML response, we might want different headers if the API is sensitive,
+ # but httpx usually handles basic GET requests well.
+ response = await self.http_client.get(document_api_url)
+ response.raise_for_status()
+
+ # Danıştay /getDokuman directly returns HTML text
+ html_content_from_api = response.text
+
+ if not isinstance(html_content_from_api, str) or not html_content_from_api.strip():
+ logger.warning(f"DanistayApiClient: Received empty or non-string HTML content for ID {id}.")
+ # Return with None markdown_content if HTML is effectively empty
+ return DanistayDocumentMarkdown(
+ id=id,
+ markdown_content=None,
+ source_url=source_url
+ )
+
+ markdown_content = self._convert_html_to_markdown_danistay(html_content_from_api)
+
+ return DanistayDocumentMarkdown(
+ id=id,
+ markdown_content=markdown_content,
+ source_url=source_url
+ )
+ except httpx.RequestError as e:
+ logger.error(f"DanistayApiClient: HTTP error fetching Danistay document (ID: {id}): {e}")
+ raise
+ # Removed ValueError for JSON as Danistay /getDokuman returns direct HTML
+ except Exception as e: # Catches other errors like MarkItDown issues if they propagate
+ logger.error(f"DanistayApiClient: General error processing Danistay document (ID: {id}): {e}")
+ raise
+
+ async def close_client_session(self):
+ """Closes the HTTPX client session."""
+ if self.http_client and not self.http_client.is_closed:
+ await self.http_client.aclose()
+ logger.info("DanistayApiClient: HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py
new file mode 100644
index 0000000..8c657a9
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py
@@ -0,0 +1,112 @@
+# danistay_mcp_module/models.py
+
+from pydantic import BaseModel, Field, HttpUrl, ConfigDict
+from typing import List, Optional, Dict, Any
+
+class DanistayBaseSearchRequest(BaseModel):
+ """Base model for common search parameters for Danistay."""
+ pageSize: int = Field(default=10, ge=1, le=10)
+ pageNumber: int = Field(default=1, ge=1)
+ # siralama and siralamaDirection are part of detailed search, not necessarily keyword search
+ # as per user's provided payloads.
+
+class DanistayKeywordSearchRequestData(BaseModel):
+ """Internal data model for the keyword search payload's 'data' field."""
+ andKelimeler: List[str] = Field(default_factory=list)
+ orKelimeler: List[str] = Field(default_factory=list)
+ notAndKelimeler: List[str] = Field(default_factory=list)
+ notOrKelimeler: List[str] = Field(default_factory=list)
+ pageSize: int
+ pageNumber: int
+
+class DanistayKeywordSearchRequest(BaseModel): # This is the model the MCP tool will accept
+ """Model for keyword-based search request for Danistay."""
+ andKelimeler: List[str] = Field(default_factory=list, description="AND keywords")
+ orKelimeler: List[str] = Field(default_factory=list, description="OR keywords")
+ notAndKelimeler: List[str] = Field(default_factory=list, description="NOT AND keywords")
+ notOrKelimeler: List[str] = Field(default_factory=list, description="NOT OR keywords")
+ pageSize: int = Field(default=10, ge=1, le=10)
+ pageNumber: int = Field(default=1, ge=1)
+
+class DanistayDetailedSearchRequestData(BaseModel): # Internal data model for detailed search payload
+ """Internal data model for the detailed search payload's 'data' field."""
+ daire: Optional[str] = "" # API expects empty string for None
+ esasYil: Optional[str] = ""
+ esasIlkSiraNo: Optional[str] = ""
+ esasSonSiraNo: Optional[str] = ""
+ kararYil: Optional[str] = ""
+ kararIlkSiraNo: Optional[str] = ""
+ kararSonSiraNo: Optional[str] = ""
+ baslangicTarihi: Optional[str] = ""
+ bitisTarihi: Optional[str] = ""
+ mevzuatNumarasi: Optional[str] = ""
+ mevzuatAdi: Optional[str] = ""
+ madde: Optional[str] = ""
+ siralama: str # Seems mandatory in detailed search payload
+ siralamaDirection: str # Seems mandatory
+ pageSize: int
+ pageNumber: int
+ # Note: 'arananKelime' is not in the detailed search payload example provided by user.
+ # If it can be included, it should be added here.
+
+class DanistayDetailedSearchRequest(DanistayBaseSearchRequest): # MCP tool will accept this
+ """Model for detailed search request for Danistay."""
+ daire: str = Field("", description="Chamber")
+ esasYil: str = Field("", description="Case year")
+ esasIlkSiraNo: str = Field("", description="Start case no")
+ esasSonSiraNo: str = Field("", description="End case no")
+ kararYil: str = Field("", description="Decision year")
+ kararIlkSiraNo: str = Field("", description="Start decision no")
+ kararSonSiraNo: str = Field("", description="End decision no")
+ baslangicTarihi: str = Field("", description="Start date")
+ bitisTarihi: str = Field("", description="End date")
+ mevzuatNumarasi: str = Field("", description="Law number")
+ mevzuatAdi: str = Field("", description="Law name")
+ madde: str = Field("", description="Article")
+ # Add a general keyword field if detailed search also supports it
+ # arananKelime: Optional[str] = Field(None, description="General keyword for detailed search.")
+
+
+class DanistayApiDecisionEntry(BaseModel):
+ """Model for an individual decision entry from the Danistay API search response.
+ Based on user-provided response samples for both keyword and detailed search.
+ """
+ id: str
+ # The API response for keyword search uses "daireKurul", detailed search example uses "daire".
+ # We use an alias to handle both and map to a consistent field name "chamber".
+ chamber: str = Field("", alias="daire", description="Chamber")
+ esasNo: str = Field("", description="Case number")
+ kararNo: str = Field("", description="Decision number")
+ kararTarihi: str = Field("", description="Decision date")
+ arananKelime: str = Field("", description="Keyword")
+ # index: Optional[int] = None # Present in response, can be added if needed by MCP tool
+ # siraNo: Optional[int] = None # Present in detailed response, can be added
+
+ document_url: Optional[HttpUrl] = Field(None, description="Document URL")
+
+ model_config = ConfigDict(populate_by_name=True, extra='ignore') # Important for alias to work and ignore extra fields
+
+class DanistayApiResponseInnerData(BaseModel):
+ """Model for the inner 'data' object in the Danistay API search response."""
+ data: List[DanistayApiDecisionEntry]
+ recordsTotal: int
+ recordsFiltered: int
+ draw: int = Field(0, description="Draw counter")
+
+class DanistayApiResponse(BaseModel):
+ """Model for the complete search response from the Danistay API."""
+ data: Optional[DanistayApiResponseInnerData] = Field(None, description="Response data, can be null when no results found")
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata (Meta Veri) from API.")
+
+class DanistayDocumentMarkdown(BaseModel):
+ """Model for a Danistay decision document, containing only Markdown content."""
+ id: str
+ markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.")
+ source_url: HttpUrl
+
+class CompactDanistaySearchResult(BaseModel):
+ """A compact search result model for the MCP tool to return."""
+ decisions: List[DanistayApiDecisionEntry]
+ total_records: int
+ requested_page: int
+ page_size: int
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml b/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml
new file mode 100644
index 0000000..d36eee7
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml
@@ -0,0 +1,66 @@
+version: '3.8'
+
+services:
+ yargi-mcp:
+ build: .
+ image: yargi-mcp:latest
+ container_name: yargi-mcp-server
+ ports:
+ - "${PORT:-8000}:8000"
+ environment:
+ - HOST=0.0.0.0
+ - PORT=8000
+ - LOG_LEVEL=${LOG_LEVEL:-info}
+ - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*}
+ - API_TOKEN=${API_TOKEN:-}
+ - PYTHONUNBUFFERED=1
+ volumes:
+ # Mount logs directory
+ - ./logs:/app/logs
+ # Mount .env file if it exists
+ - ./.env:/app/.env:ro
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
+ networks:
+ - yargi-network
+
+ # Optional: Nginx reverse proxy
+ nginx:
+ image: nginx:alpine
+ container_name: yargi-nginx
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./ssl:/etc/nginx/ssl:ro
+ depends_on:
+ - yargi-mcp
+ networks:
+ - yargi-network
+ profiles:
+ - production
+
+ # Optional: Redis for caching (future enhancement)
+ redis:
+ image: redis:alpine
+ container_name: yargi-redis
+ command: redis-server --appendonly yes
+ volumes:
+ - redis-data:/data
+ networks:
+ - yargi-network
+ profiles:
+ - with-cache
+
+networks:
+ yargi-network:
+ driver: bridge
+
+volumes:
+ redis-data:
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md b/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md
new file mode 100644
index 0000000..db5f361
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md
@@ -0,0 +1,428 @@
+# Yargı MCP Server Dağıtım Rehberi
+
+Bu rehber, Yargı MCP Server'ın ASGI web servisi olarak çeşitli dağıtım seçeneklerini kapsar.
+
+## İçindekiler
+
+- [Hızlı Başlangıç](#hızlı-başlangıç)
+- [Yerel Geliştirme](#yerel-geliştirme)
+- [Production Dağıtımı](#production-dağıtımı)
+- [Cloud Dağıtımı](#cloud-dağıtımı)
+- [Docker Dağıtımı](#docker-dağıtımı)
+- [Güvenlik Hususları](#güvenlik-hususları)
+- [İzleme](#izleme)
+
+## Hızlı Başlangıç
+
+### 1. Bağımlılıkları Yükleyin
+
+```bash
+# ASGI sunucusu için uvicorn yükleyin
+pip install uvicorn
+
+# Veya tüm bağımlılıklarla birlikte yükleyin
+pip install -e .
+pip install uvicorn
+```
+
+### 2. Sunucuyu Çalıştırın
+
+```bash
+# Temel başlatma
+python run_asgi.py
+
+# Veya doğrudan uvicorn ile
+uvicorn asgi_app:app --host 0.0.0.0 --port 8000
+```
+
+Sunucu şu adreslerde kullanılabilir olacak:
+- MCP Endpoint: `http://localhost:8000/mcp/`
+- Sağlık Kontrolü: `http://localhost:8000/health`
+- API Durumu: `http://localhost:8000/status`
+
+## Yerel Geliştirme
+
+### Otomatik Yeniden Yükleme ile Geliştirme Sunucusu
+
+```bash
+python run_asgi.py --reload --log-level debug
+```
+
+### FastAPI Entegrasyonunu Kullanma
+
+Ek REST API endpoint'leri için:
+
+```bash
+uvicorn fastapi_app:app --reload
+```
+
+Bu şunları sağlar:
+- `/docs` adresinde interaktif API dokümantasyonu
+- `/api/tools` adresinde araç listesi
+- `/api/databases` adresinde veritabanı bilgileri
+
+### Ortam Değişkenleri
+
+`.env.example` dosyasını temel alarak bir `.env` dosyası oluşturun:
+
+```bash
+cp .env.example .env
+```
+
+Temel değişkenler:
+- `HOST`: Sunucu host adresi (varsayılan: 127.0.0.1)
+- `PORT`: Sunucu portu (varsayılan: 8000)
+- `ALLOWED_ORIGINS`: CORS kökenleri (virgülle ayrılmış)
+- `LOG_LEVEL`: Log seviyesi (debug, info, warning, error)
+
+## Production Dağıtımı
+
+### 1. Uvicorn ile Çoklu Worker Kullanımı
+
+```bash
+python run_asgi.py --host 0.0.0.0 --port 8000 --workers 4
+```
+
+### 2. Gunicorn Kullanımı
+
+```bash
+pip install gunicorn
+gunicorn asgi_app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
+```
+
+### 3. Nginx Reverse Proxy ile
+
+1. Nginx'i yükleyin
+2. Sağlanan `nginx.conf` dosyasını kullanın:
+
+```bash
+sudo cp nginx.conf /etc/nginx/sites-available/yargi-mcp
+sudo ln -s /etc/nginx/sites-available/yargi-mcp /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl reload nginx
+```
+
+### 4. Systemd Servisi
+
+`/etc/systemd/system/yargi-mcp.service` dosyasını oluşturun:
+
+```ini
+[Unit]
+Description=Yargı MCP Server
+After=network.target
+
+[Service]
+Type=exec
+User=www-data
+WorkingDirectory=/opt/yargi-mcp
+Environment="PATH=/opt/yargi-mcp/venv/bin"
+ExecStart=/opt/yargi-mcp/venv/bin/uvicorn asgi_app:app --host 0.0.0.0 --port 8000 --workers 4
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Etkinleştirin ve başlatın:
+
+```bash
+sudo systemctl enable yargi-mcp
+sudo systemctl start yargi-mcp
+```
+
+## Cloud Dağıtımı
+
+### Heroku
+
+1. `Procfile` oluşturun:
+```
+web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT
+```
+
+2. Dağıtın:
+```bash
+heroku create uygulama-isminiz
+git push heroku main
+```
+
+### Railway
+
+1. `railway.json` ekleyin:
+```json
+{
+ "build": {
+ "builder": "NIXPACKS"
+ },
+ "deploy": {
+ "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT"
+ }
+}
+```
+
+2. Railway CLI veya GitHub entegrasyonu ile dağıtın
+
+### Google Cloud Run
+
+1. Container oluşturun:
+```bash
+docker build -t yargi-mcp .
+docker tag yargi-mcp gcr.io/PROJE_ADINIZ/yargi-mcp
+docker push gcr.io/PROJE_ADINIZ/yargi-mcp
+```
+
+2. Dağıtın:
+```bash
+gcloud run deploy yargi-mcp \
+ --image gcr.io/PROJE_ADINIZ/yargi-mcp \
+ --platform managed \
+ --region us-central1 \
+ --allow-unauthenticated
+```
+
+### AWS Lambda (Mangum kullanarak)
+
+1. Mangum'u yükleyin:
+```bash
+pip install mangum
+```
+
+2. `lambda_handler.py` oluşturun:
+```python
+from mangum import Mangum
+from asgi_app import app
+
+handler = Mangum(app, lifespan="off")
+```
+
+3. AWS SAM veya Serverless Framework kullanarak dağıtın
+
+## Docker Dağıtımı
+
+### Tek Container
+
+```bash
+# Oluşturun
+docker build -t yargi-mcp .
+
+# Çalıştırın
+docker run -p 8000:8000 --env-file .env yargi-mcp
+```
+
+### Docker Compose
+
+```bash
+# Geliştirme
+docker-compose up
+
+# Nginx ile Production
+docker-compose --profile production up
+
+# Redis önbellekleme ile
+docker-compose --profile with-cache up
+```
+
+### Kubernetes
+
+Deployment YAML oluşturun:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: yargi-mcp
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: yargi-mcp
+ template:
+ metadata:
+ labels:
+ app: yargi-mcp
+ spec:
+ containers:
+ - name: yargi-mcp
+ image: yargi-mcp:latest
+ ports:
+ - containerPort: 8000
+ env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "8000"
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8000
+ initialDelaySeconds: 10
+ periodSeconds: 30
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: yargi-mcp-service
+spec:
+ selector:
+ app: yargi-mcp
+ ports:
+ - port: 80
+ targetPort: 8000
+ type: LoadBalancer
+```
+
+## Güvenlik Hususları
+
+### 1. Kimlik Doğrulama
+
+`API_TOKEN` ortam değişkenini ayarlayarak token kimlik doğrulamasını etkinleştirin:
+
+```bash
+export API_TOKEN=gizli-token-degeri
+```
+
+Ardından isteklere ekleyin:
+```bash
+curl -H "Authorization: Bearer gizli-token-degeri" http://localhost:8000/api/tools
+```
+
+### 2. HTTPS/SSL
+
+Production için her zaman HTTPS kullanın:
+
+1. SSL sertifikası edinin (Let's Encrypt vb.)
+2. Nginx veya cloud sağlayıcıda yapılandırın
+3. `ALLOWED_ORIGINS` değerini https:// kullanacak şekilde güncelleyin
+
+### 3. Rate Limiting (Hız Sınırlama)
+
+Sağlanan Nginx yapılandırması rate limiting içerir:
+- API endpoint'leri: 10 istek/saniye
+- MCP endpoint: 100 istek/saniye
+
+### 4. CORS Yapılandırması
+
+Production için belirli kaynaklara izin verin:
+
+```bash
+ALLOWED_ORIGINS=https://app.sizindomain.com,https://www.sizindomain.com
+```
+
+## İzleme
+
+### Sağlık Kontrolleri
+
+`/health` endpoint'ini izleyin:
+
+```bash
+curl http://localhost:8000/health
+```
+
+Yanıt:
+```json
+{
+ "status": "healthy",
+ "timestamp": "2024-12-26T10:00:00",
+ "uptime_seconds": 3600,
+ "tools_operational": true
+}
+```
+
+### Loglama
+
+Ortam değişkeni ile log seviyesini yapılandırın:
+
+```bash
+LOG_LEVEL=info # veya debug, warning, error
+```
+
+Loglar şuraya yazılır:
+- Konsol (stdout)
+- `logs/mcp_server.log` dosyası
+
+### Metrikler (Opsiyonel)
+
+OpenTelemetry desteği için:
+
+```bash
+pip install opentelemetry-instrumentation-fastapi
+```
+
+Ortam değişkenlerini ayarlayın:
+```bash
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+OTEL_SERVICE_NAME=yargi-mcp-server
+```
+
+## Sorun Giderme
+
+### Port Zaten Kullanımda
+
+```bash
+# 8000 portunu kullanan işlemi bulun
+lsof -i :8000
+
+# İşlemi sonlandırın
+kill -9
+```
+
+### İzin Hataları
+
+Dosya izinlerinin doğru olduğundan emin olun:
+
+```bash
+chmod +x run_asgi.py
+chown -R www-data:www-data /opt/yargi-mcp
+```
+
+### Bellek Sorunları
+
+Büyük belge işleme için worker belleğini artırın:
+
+```bash
+# systemd servisinde
+Environment="PYTHONMALLOC=malloc"
+LimitNOFILE=65536
+```
+
+### Zaman Aşımı Sorunları
+
+Zaman aşımlarını ayarlayın:
+1. Uvicorn: `--timeout-keep-alive 75`
+2. Nginx: `proxy_read_timeout 300s;`
+3. Cloud sağlayıcılar: Platform özel zaman aşımı ayarlarını kontrol edin
+
+## Performans Ayarlama
+
+### 1. Worker İşlemleri
+
+- Geliştirme: 1 worker
+- Production: CPU çekirdeği başına 2-4 worker
+
+### 2. Bağlantı Havuzlama
+
+Sunucu varsayılan olarak httpx ile bağlantı havuzlama kullanır.
+
+### 3. Önbellekleme (Gelecek Geliştirme)
+
+Redis önbellekleme docker-compose ile etkinleştirilebilir:
+
+```bash
+docker-compose --profile with-cache up
+```
+
+### 4. Veritabanı Zaman Aşımları
+
+`.env` dosyasında veritabanı başına zaman aşımlarını ayarlayın:
+
+```bash
+YARGITAY_TIMEOUT=60
+DANISTAY_TIMEOUT=60
+ANAYASA_TIMEOUT=90
+```
+
+## Destek
+
+Sorunlar ve sorular için:
+- GitHub Issues: https://github.com/saidsurucu/yargi-mcp/issues
+- Dokümantasyon: README.md dosyasına bakın
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py
new file mode 100644
index 0000000..235d515
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py
@@ -0,0 +1,177 @@
+# emsal_mcp_module/client.py
+
+import httpx
+# from bs4 import BeautifulSoup # Uncomment if needed for advanced HTML pre-processing
+from typing import Dict, Any, List, Optional
+import logging
+import html
+import re
+import io
+from markitdown import MarkItDown
+
+from .models import (
+ EmsalSearchRequest,
+ EmsalDetailedSearchRequestData,
+ EmsalApiResponse,
+ EmsalDocumentMarkdown
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+class EmsalApiClient:
+ """API Client for Emsal (UYAP Precedent Decision) search system."""
+ BASE_URL = "https://emsal.uyap.gov.tr"
+ DETAILED_SEARCH_ENDPOINT = "/aramadetaylist"
+ DOCUMENT_ENDPOINT = "/getDokuman"
+
+ def __init__(self, request_timeout: float = 30.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Content-Type": "application/json; charset=UTF-8",
+ "Accept": "application/json, text/plain, */*",
+ "X-Requested-With": "XMLHttpRequest",
+ },
+ timeout=request_timeout,
+ verify=False # As per user's original FastAPI code
+ )
+
+ async def search_detailed_decisions(
+ self,
+ params: EmsalSearchRequest
+ ) -> EmsalApiResponse:
+ """Performs a detailed search for Emsal decisions."""
+
+ data_for_api_payload = EmsalDetailedSearchRequestData(
+ arananKelime=params.keyword or "",
+ Bam_Hukuk_Mahkemeleri=params.selected_bam_civil_court, # Uses alias "Bam Hukuk Mahkemeleri"
+ Hukuk_Mahkemeleri=params.selected_civil_court, # Uses alias "Hukuk Mahkemeleri"
+ birimHukukMah="+".join(params.selected_regional_civil_chambers) if params.selected_regional_civil_chambers else "",
+ esasYil=params.case_year_esas or "",
+ esasIlkSiraNo=params.case_start_seq_esas or "",
+ esasSonSiraNo=params.case_end_seq_esas or "",
+ kararYil=params.decision_year_karar or "",
+ kararIlkSiraNo=params.decision_start_seq_karar or "",
+ kararSonSiraNo=params.decision_end_seq_karar or "",
+ baslangicTarihi=params.start_date or "",
+ bitisTarihi=params.end_date or "",
+ siralama=params.sort_criteria,
+ siralamaDirection=params.sort_direction,
+ pageSize=params.page_size,
+ pageNumber=params.page_number
+ )
+
+ # Create request dict and remove empty string fields to avoid API issues
+ payload_dict = data_for_api_payload.model_dump(by_alias=True, exclude_none=True)
+ # Remove empty string fields that might cause API issues
+ cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""}
+ final_payload = {"data": cleaned_payload}
+
+ logger.info(f"EmsalApiClient: Performing DETAILED search with payload: {final_payload}")
+ return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload)
+
+ async def _execute_api_search(self, endpoint: str, payload: Dict) -> EmsalApiResponse:
+ """Helper method to execute search POST request and process response for Emsal."""
+ try:
+ response = await self.http_client.post(endpoint, json=payload)
+ response.raise_for_status()
+ response_json_data = response.json()
+ logger.debug(f"EmsalApiClient: Raw API response from {endpoint}: {response_json_data}")
+
+ api_response_parsed = EmsalApiResponse(**response_json_data)
+
+ if api_response_parsed.data and api_response_parsed.data.data:
+ for decision_item in api_response_parsed.data.data:
+ if decision_item.id:
+ decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}"
+
+ return api_response_parsed
+ except httpx.RequestError as e:
+ logger.error(f"EmsalApiClient: HTTP request error during Emsal search to {endpoint}: {e}")
+ raise
+ except Exception as e:
+ logger.error(f"EmsalApiClient: Error processing or validating Emsal search response from {endpoint}: {e}")
+ raise
+
+ def _clean_html_and_convert_to_markdown_emsal(self, html_content_from_api_data_field: str) -> Optional[str]:
+ """
+ Cleans HTML (from Emsal API 'data' field containing HTML string)
+ and converts it to Markdown using MarkItDown.
+ This assumes Emsal /getDokuman response is JSON with HTML in "data" field,
+ similar to Yargitay and the last Emsal /getDokuman example.
+ """
+ if not html_content_from_api_data_field:
+ return None
+
+ # Basic HTML unescaping and fixing common escaped characters
+ # Based on user's original fix_html_content in app/routers/emsal.py
+ content = html.unescape(html_content_from_api_data_field)
+ content = content.replace('\\"', '"')
+ content = content.replace('\\r\\n', '\n')
+ content = content.replace('\\n', '\n')
+ content = content.replace('\\t', '\t')
+
+ # The HTML string from "data" field starts with "..."
+ html_input_for_markdown = content
+
+ markdown_text = None
+ try:
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_input_for_markdown.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown()
+ conversion_result = md_converter.convert(html_stream)
+ markdown_text = conversion_result.text_content
+ logger.info("EmsalApiClient: HTML to Markdown conversion successful.")
+ except Exception as e:
+ logger.error(f"EmsalApiClient: Error during MarkItDown HTML to Markdown conversion for Emsal: {e}")
+
+ return markdown_text
+
+ async def get_decision_document_as_markdown(self, id: str) -> EmsalDocumentMarkdown:
+ """
+ Retrieves a specific Emsal decision by ID and returns its content as Markdown.
+ Assumes Emsal /getDokuman endpoint returns JSON with HTML content in the 'data' field.
+ """
+ document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={id}"
+ source_url = f"{self.BASE_URL}{document_api_url}"
+ logger.info(f"EmsalApiClient: Fetching Emsal document for Markdown (ID: {id}) from {source_url}")
+
+ try:
+ response = await self.http_client.get(document_api_url)
+ response.raise_for_status()
+
+ # Emsal /getDokuman returns JSON with HTML in 'data' field (confirmed by user example)
+ response_json = response.json()
+ html_content_from_api = response_json.get("data")
+
+ if not isinstance(html_content_from_api, str) or not html_content_from_api.strip():
+ logger.warning(f"EmsalApiClient: Received empty or non-string HTML in 'data' field for Emsal ID {id}.")
+ return EmsalDocumentMarkdown(id=id, markdown_content=None, source_url=source_url)
+
+ markdown_content = self._clean_html_and_convert_to_markdown_emsal(html_content_from_api)
+
+ return EmsalDocumentMarkdown(
+ id=id,
+ markdown_content=markdown_content,
+ source_url=source_url
+ )
+ except httpx.RequestError as e:
+ logger.error(f"EmsalApiClient: HTTP error fetching Emsal document (ID: {id}): {e}")
+ raise
+ except ValueError as e:
+ logger.error(f"EmsalApiClient: ValueError processing Emsal document response (ID: {id}): {e}")
+ raise
+ except Exception as e:
+ logger.error(f"EmsalApiClient: General error processing Emsal document (ID: {id}): {e}")
+ raise
+
+ async def close_client_session(self):
+ """Closes the HTTPX client session."""
+ if self.http_client and not self.http_client.is_closed:
+ await self.http_client.aclose()
+ logger.info("EmsalApiClient: HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py
new file mode 100644
index 0000000..c94f0dd
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py
@@ -0,0 +1,101 @@
+# emsal_mcp_module/models.py
+
+from pydantic import BaseModel, Field, HttpUrl, ConfigDict
+from typing import List, Optional, Dict, Any
+
+class EmsalDetailedSearchRequestData(BaseModel):
+ """
+ Internal model for the 'data' object in the Emsal detailed search payload.
+ Field names use aliases to match the exact keys in the API payload
+ (e.g., "Bam Hukuk Mahkemeleri" with spaces).
+ The API expects empty strings for None/omitted optional fields.
+ """
+ arananKelime: Optional[str] = ""
+
+ Bam_Hukuk_Mahkemeleri: str = Field("", alias="Bam Hukuk Mahkemeleri")
+ Hukuk_Mahkemeleri: str = Field("", alias="Hukuk Mahkemeleri")
+ # Add other specific court type fields from the form if they are separate keys in payload
+ # E.g., "Ceza Mahkemeleri", "İdari Mahkemeler" etc.
+
+ birimHukukMah: Optional[str] = Field("", description="Regional chambers (+ separated)")
+
+ esasYil: Optional[str] = ""
+ esasIlkSiraNo: Optional[str] = ""
+ esasSonSiraNo: Optional[str] = ""
+ kararYil: Optional[str] = ""
+ kararIlkSiraNo: Optional[str] = ""
+ kararSonSiraNo: Optional[str] = ""
+ baslangicTarihi: Optional[str] = ""
+ bitisTarihi: Optional[str] = ""
+ siralama: str # Mandatory in payload example
+ siralamaDirection: str # Mandatory in payload example
+ pageSize: int
+ pageNumber: int
+
+ model_config = ConfigDict(populate_by_name=True) # Enables use of alias in serialization (when dumping to dict for payload)
+
+class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accept
+ """Model for Emsal detailed search request, with user-friendly field names."""
+ keyword: str = Field("", description="Keyword")
+
+ selected_bam_civil_court: str = Field("", description="BAM Civil Court")
+ selected_civil_court: str = Field("", description="Civil Court")
+ selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Regional chambers")
+
+ case_year_esas: str = Field("", description="Case year")
+ case_start_seq_esas: str = Field("", description="Start case no")
+ case_end_seq_esas: str = Field("", description="End case no")
+
+ decision_year_karar: str = Field("", description="Decision year")
+ decision_start_seq_karar: str = Field("", description="Start decision no")
+ decision_end_seq_karar: str = Field("", description="End decision no")
+
+ start_date: str = Field("", description="Start date (DD.MM.YYYY)")
+ end_date: str = Field("", description="End date (DD.MM.YYYY)")
+
+ sort_criteria: str = Field("1", description="Sort by")
+ sort_direction: str = Field("desc", description="Direction")
+
+ page_number: int = Field(default=1, ge=1)
+ page_size: int = Field(default=10, ge=1, le=10)
+
+
+class EmsalApiDecisionEntry(BaseModel):
+ """Model for an individual decision entry from the Emsal API search response."""
+ id: str
+ daire: str = Field("", description="Chamber")
+ esasNo: str = Field("", description="Case number")
+ kararNo: str = Field("", description="Decision number")
+ kararTarihi: str = Field("", description="Decision date")
+ arananKelime: str = Field("", description="Keyword")
+ durum: str = Field("", description="Status")
+ # index: Optional[int] = None # Present in Emsal response, can be added if tool needs it
+
+ document_url: Optional[HttpUrl] = Field(None, description="Document URL")
+
+ model_config = ConfigDict(extra='ignore')
+
+class EmsalApiResponseInnerData(BaseModel):
+ """Model for the inner 'data' object in the Emsal API search response."""
+ data: List[EmsalApiDecisionEntry]
+ recordsTotal: int
+ recordsFiltered: int
+ draw: int = Field(0, description="Draw counter (Çizim Sayıcısı) from API, usually for DataTables.")
+
+class EmsalApiResponse(BaseModel):
+ """Model for the complete search response from the Emsal API."""
+ data: EmsalApiResponseInnerData
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata (Meta Veri) from API, if any.")
+
+class EmsalDocumentMarkdown(BaseModel):
+ """Model for an Emsal decision document, containing only Markdown content."""
+ id: str
+ markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.")
+ source_url: HttpUrl
+
+class CompactEmsalSearchResult(BaseModel):
+ """A compact search result model for the MCP tool to return."""
+ decisions: List[EmsalApiDecisionEntry]
+ total_records: int
+ requested_page: int
+ page_size: int
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py b/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py
new file mode 100644
index 0000000..5e4c3fd
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py
@@ -0,0 +1,1680 @@
+"""
+FastAPI Comprehensive Endpoints with Complete MCP Documentation
+This is the complete version with all descriptions and docstrings from MCP server.
+"""
+
+import os
+from typing import List, Dict, Any, Optional
+from datetime import datetime
+
+from fastapi import FastAPI, HTTPException, Query, Depends, Body
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel, Field
+import json
+
+# Import the main MCP app
+from mcp_server_main import app as mcp_server
+
+# Create MCP ASGI app
+mcp_asgi_app = mcp_server.http_app(path="/mcp")
+
+# Create FastAPI app with MCP lifespan
+app = FastAPI(
+ title="Yargı MCP API - Turkish Legal Database REST API",
+ description="""
+ Comprehensive REST API for Turkish Legal Databases with complete MCP tool coverage.
+
+ This API provides access to 8 major Turkish legal institutions including:
+ • Yargıtay (Court of Cassation) - Supreme civil/criminal court
+ • Danıştay (Council of State) - Supreme administrative court
+ • Constitutional Court - Constitutional review and individual applications
+ • Competition Authority - Antitrust and merger decisions
+ • Public Procurement Authority - Government contracting disputes
+ • Court of Accounts - Public audit and accountability
+ • Emsal (UYAP Precedents) - Cross-court precedent database
+ • Local and Appellate Courts - First and second instance decisions
+
+ Features complete coverage of 33 MCP tools with enhanced documentation,
+ typed request models, and comprehensive legal context.
+
+ Tool Properties (from MCP annotations):
+ • Read-only: All tools are read-only and do not modify system state
+ • Idempotent: Same inputs produce same outputs for reliable research
+ • Open-world search: Search tools explore comprehensive legal databases
+ • Deterministic document retrieval: Document tools return consistent content
+ """,
+ version="1.0.0",
+ lifespan=mcp_asgi_app.lifespan
+)
+
+# Add CORS middleware
+cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Mount MCP server
+app.mount("/mcp-server", mcp_asgi_app)
+
+# Response models (keeping from original)
+class ToolInfo(BaseModel):
+ name: str
+ description: str
+ parameters: Dict[str, Any]
+
+class ServerInfo(BaseModel):
+ name: str
+ version: str
+ description: str
+ tools_count: int
+ databases: List[str]
+ mcp_endpoint: str
+ api_docs: str
+
+class HealthCheck(BaseModel):
+ status: str
+ timestamp: datetime
+ uptime_seconds: Optional[float] = None
+ tools_operational: bool
+
+# Track server start time
+SERVER_START_TIME = datetime.now()
+
+# MCP tool caller helper
+async def call_mcp_tool(tool_name: str, arguments: Dict[str, Any]):
+ """Call an MCP tool with given arguments"""
+ try:
+ tool = mcp_server._tool_manager._tools.get(tool_name)
+ if not tool:
+ raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
+ result = await tool.fn(**arguments)
+ return result
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Tool execution failed: {str(e)}")
+
+# ============================================================================
+# COMPREHENSIVE REQUEST MODELS WITH FULL MCP DOCUMENTATION
+# ============================================================================
+
+class YargitaySearchRequest(BaseModel):
+ """
+ Search request for Court of Cassation (Yargıtay) decisions using primary official API.
+
+ The Court of Cassation is Turkey's highest court for civil and criminal matters,
+ equivalent to a Supreme Court. Provides access to comprehensive supreme court precedents.
+ """
+ arananKelime: str = Field(
+ ...,
+ description="""Keyword to search for with advanced operators:
+ • Space between words = OR logic (arsa payı → "arsa" OR "payı")
+ • "exact phrase" = Exact match ("arsa payı" → exact phrase)
+ • word1+word2 = AND logic (arsa+payı → both words required)
+ • word* = Wildcard (bozma* → bozma, bozması, bozmanın, etc.)
+ • +"phrase1" +"phrase2" = Multiple required phrases
+ • +"required" -"excluded" = Include and exclude
+
+ Turkish Examples:
+ • Simple OR: arsa payı (~523K results)
+ • Exact phrase: "arsa payı" (~22K results)
+ • Multiple AND: +"arsa payı" +"bozma sebebi" (~234 results)
+ • Wildcard: bozma* (bozma, bozması, bozmanın, etc.)
+ • Exclude: +"arsa payı" -"kira sözleşmesi"
+ """,
+ example='+"mülkiyet hakkı" +"iptal"'
+ )
+ birimYrgKurulDaire: Optional[str] = Field(
+ "",
+ description="""Chamber/board selection (52 options):
+ Civil Chambers: 1-23. Hukuk Dairesi
+ Criminal Chambers: 1-23. Ceza Dairesi
+ General Assemblies: Hukuk Genel Kurulu, Ceza Genel Kurulu
+ Special Boards: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu
+
+ Use "" for ALL chambers or specify exact chamber name.
+ """,
+ example="1. Hukuk Dairesi"
+ )
+ baslangicTarihi: Optional[str] = Field(None, description="Start date (DD.MM.YYYY)", example="01.01.2020")
+ bitisTarihi: Optional[str] = Field(None, description="End date (DD.MM.YYYY)", example="31.12.2024")
+ pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100, example=20)
+
+class YargitayBedestenSearchRequest(BaseModel):
+ """
+ Search request for Court of Cassation using Bedesten API (alternative source).
+ Complements primary API with different search capabilities and recent decisions.
+ """
+ phrase: str = Field(
+ ...,
+ description="""Aranacak kavram/kelime. İki farklı arama türü desteklenir:
+ • Normal arama: "mülkiyet hakkı" - kelimeler ayrı ayrı aranır
+ • Tam cümle arama: "\"mülkiyet hakkı\"" - tırnak içindeki ifade aynen aranır
+ Tam cümle aramalar daha kesin sonuçlar verir.
+
+ Search phrase with exact matching support:
+ • Regular search: "mülkiyet hakkı" - searches individual words separately
+ • Exact phrase search: "\"mülkiyet hakkı\"" - searches for exact phrase as unit
+ Exact phrase search provides more precise results with fewer false positives.
+ """,
+ example="\"mülkiyet hakkı\""
+ )
+ birimAdi: Optional[str] = Field(
+ None,
+ description="""Daire/Kurul seçimi (52 seçenek - ana API ile aynı):
+ • Hukuk daireleri: 1. Hukuk Dairesi - 23. Hukuk Dairesi
+ • Ceza daireleri: 1. Ceza Dairesi - 23. Ceza Dairesi
+ • Genel kurullar: Hukuk Genel Kurulu, Ceza Genel Kurulu
+ • Özel kurullar: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu
+
+ Chamber filtering (52 options - same as primary API):
+ • Civil chambers: 1. Hukuk Dairesi through 23. Hukuk Dairesi
+ • Criminal chambers: 1. Ceza Dairesi through 23. Ceza Dairesi
+ • General assemblies: Hukuk Genel Kurulu, Ceza Genel Kurulu
+ • Special boards: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu
+
+ Use None for ALL chambers, or specify exact chamber name.
+ """,
+ example="1. Hukuk Dairesi"
+ )
+ kararTarihiStart: Optional[str] = Field(
+ None,
+ description="""Karar başlangıç tarihi (ISO 8601 formatı):
+ Format: YYYY-MM-DDTHH:MM:SS.000Z
+ Örnek: "2024-01-01T00:00:00.000Z" - 1 Ocak 2024'ten itibaren kararlar
+ kararTarihiEnd ile birlikte tarih aralığı filtrelemesi için kullanılır.
+
+ Decision start date filter (ISO 8601 format):
+ Format: YYYY-MM-DDTHH:MM:SS.000Z
+ Example: "2024-01-01T00:00:00.000Z" for decisions from Jan 1, 2024
+ Use with kararTarihiEnd for date range filtering.
+ """,
+ example="2024-01-01T00:00:00.000Z"
+ )
+ kararTarihiEnd: Optional[str] = Field(
+ None,
+ description="""Karar bitiş tarihi (ISO 8601 formatı):
+ Format: YYYY-MM-DDTHH:MM:SS.000Z
+ Örnek: "2024-12-31T23:59:59.999Z" - 31 Aralık 2024'e kadar kararlar
+ kararTarihiStart ile birlikte tarih aralığı filtrelemesi için kullanılır.
+
+ Decision end date filter (ISO 8601 format):
+ Format: YYYY-MM-DDTHH:MM:SS.000Z
+ Example: "2024-12-31T23:59:59.999Z" for decisions until Dec 31, 2024
+ Use with kararTarihiStart for date range filtering.
+ """,
+ example="2024-12-31T23:59:59.999Z"
+ )
+ pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100)
+
+class DanistayKeywordSearchRequest(BaseModel):
+ """
+ Keyword-based search for Council of State (Danıştay) decisions with Boolean logic.
+
+ The Council of State is Turkey's highest administrative court, providing final
+ rulings on administrative law matters with Boolean keyword operators.
+ """
+ andKelimeler: List[str] = Field(
+ ...,
+ description="ALL keywords must be present (AND logic)",
+ example=["idari işlem", "iptal"]
+ )
+ orKelimeler: Optional[List[str]] = Field(
+ None,
+ description="ANY keyword can be present (OR logic)",
+ example=["ruhsat", "izin", "lisans"]
+ )
+ notKelimeler: Optional[List[str]] = Field(
+ None,
+ description="EXCLUDE if keywords present (NOT logic)",
+ example=["vergi"]
+ )
+ pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100)
+
+class DanistayDetailedSearchRequest(BaseModel):
+ """
+ Detailed search for Council of State decisions with comprehensive filtering.
+ Provides the most comprehensive search capabilities for administrative court decisions.
+ """
+ daire: Optional[str] = Field(
+ None,
+ description="Chamber/Department filter (1. Daire through 17. Daire, special councils)",
+ example="3. Daire"
+ )
+ baslangicTarihi: Optional[str] = Field(None, description="Start date (DD.MM.YYYY)", example="01.01.2020")
+ bitisTarihi: Optional[str] = Field(None, description="End date (DD.MM.YYYY)", example="31.12.2024")
+ esas: Optional[str] = Field(None, description="Case number (Esas No)", example="2024/123")
+ karar: Optional[str] = Field(None, description="Decision number (Karar No)", example="2024/456")
+
+class DanistayBedestenSearchRequest(BaseModel):
+ """
+ Council of State search using Bedesten API with chamber filtering and exact phrase search.
+ Provides access to administrative court decisions with 27 chamber options.
+ """
+ phrase: str = Field(..., description="Search phrase (supports exact matching with quotes)")
+ birimAdi: Optional[str] = Field(
+ None,
+ description="""Chamber filtering (27 options):
+ Main Councils: Büyük Gen.Kur., İdare Dava Daireleri Kurulu, Vergi Dava Daireleri Kurulu
+ Chambers: 1. Daire through 17. Daire
+ Military: Askeri Yüksek İdare Mahkemesi chambers
+ """,
+ example="3. Daire"
+ )
+ kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601)")
+ kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601)")
+ pageSize: int = Field(20, description="Results per page", ge=1, le=100)
+
+class EmsalSearchRequest(BaseModel):
+ """
+ Search Precedent (Emsal) decisions from UYAP system across multiple court levels.
+ Provides access to precedent decisions from various Turkish courts.
+ """
+ keyword: str = Field(..., description="Search keyword across decision texts")
+ decision_year_karar: Optional[str] = Field(None, description="Decision year filter", example="2024")
+ results_per_page: int = Field(20, description="Results per page", ge=1, le=100)
+
+class UyusmazlikSearchRequest(BaseModel):
+ """
+ Search Court of Jurisdictional Disputes decisions.
+ Resolves jurisdictional disputes between different court systems.
+ """
+ keywords: List[str] = Field(..., description="Search keywords", example=["görev", "uyuşmazlık"])
+ page_to_fetch: int = Field(1, description="Page number", ge=1)
+
+class AnayasaNormSearchRequest(BaseModel):
+ """
+ Search Constitutional Court norm control (judicial review) decisions.
+ Turkey's highest constitutional authority for reviewing law constitutionality.
+ """
+ keywords_all: List[str] = Field(..., description="All required keywords", example=["eğitim hakkı", "anayasa"])
+ period: Optional[str] = Field(None, description="Constitutional period (1=1961, 2=1982)", example="2")
+ application_type: Optional[str] = Field(None, description="Application type (1=İptal)", example="1")
+ results_per_page: int = Field(20, description="Results per page", ge=1, le=100)
+
+class AnayasaBireyselSearchRequest(BaseModel):
+ """
+ Search Constitutional Court individual application decisions.
+ Human rights violation cases through individual citizen petitions.
+ """
+ keywords: List[str] = Field(..., description="Search keywords", example=["ifade özgürlüğü", "basın"])
+ page_to_fetch: int = Field(1, description="Page number", ge=1)
+
+class KikSearchRequest(BaseModel):
+ """
+ Search Public Procurement Authority (KİK) decisions.
+ Government procurement disputes and regulatory interpretations.
+ """
+ karar_tipi: Optional[str] = Field(
+ None,
+ description="Decision type (rbUyusmazlik=Disputes, rbDuzenleyici=Regulatory, rbMahkeme=Court)",
+ example="rbUyusmazlik"
+ )
+ karar_metni: Optional[str] = Field(None, description="Decision text search", example="ihale iptali")
+ basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject", example="danışmanlık")
+ karar_tarihi_baslangic: Optional[str] = Field(None, description="Start date", example="01.01.2023")
+
+class RekabetSearchRequest(BaseModel):
+ """
+ Search Competition Authority decisions.
+ Antitrust, merger control, and competition law enforcement.
+ """
+ KararTuru: Optional[str] = Field(
+ None,
+ description="Decision type (Birleşme ve Devralma, Rekabet İhlali, Muafiyet, etc.)",
+ example="Birleşme ve Devralma"
+ )
+ PdfText: Optional[str] = Field(
+ None,
+ description="Full-text search in decisions. Use quotes for exact phrases.",
+ example="\"market definition\" telecommunications"
+ )
+ YayinlanmaTarihi: Optional[str] = Field(None, description="Publication date", example="01.01.2020")
+ page: int = Field(1, description="Page number", ge=1)
+
+class BedestenSearchRequest(BaseModel):
+ """
+ Generic search request for Bedesten API courts (Yerel Hukuk, İstinaf Hukuk, KYB).
+ Supports exact phrase search and date filtering.
+ """
+ phrase: str = Field(
+ ...,
+ description="Search phrase. Use quotes for exact matching: \"legal term\"",
+ example="\"sözleşme ihlali\""
+ )
+ kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601)")
+ kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601)")
+ pageSize: int = Field(20, description="Results per page", ge=1, le=100)
+
+class SayistaySearchRequest(BaseModel):
+ """
+ Search Court of Accounts (Sayıştay) decisions.
+ Public audit, accountability, and financial oversight decisions.
+ """
+ keywords: List[str] = Field(..., description="Search keywords", example=["mali sorumluluk", "denetim"])
+ page_to_fetch: int = Field(1, description="Page number", ge=1)
+
+# ============================================================================
+# BASIC SERVER ENDPOINTS (keeping from original)
+# ============================================================================
+
+@app.get("/", response_model=ServerInfo)
+async def root():
+ """Get comprehensive server information with database coverage"""
+ return ServerInfo(
+ name="Yargı MCP Server - Turkish Legal Database API",
+ version="1.0.0",
+ description="Complete REST API for Turkish legal databases with 33 MCP tools",
+ tools_count=len(mcp_server._tool_manager._tools),
+ databases=[
+ "Yargıtay (Court of Cassation) - 4 tools",
+ "Danıştay (Council of State) - 5 tools",
+ "Emsal (UYAP Precedents) - 2 tools",
+ "Uyuşmazlık Mahkemesi (Jurisdictional Disputes) - 2 tools",
+ "Anayasa Mahkemesi (Constitutional Court) - 4 tools",
+ "Kamu İhale Kurulu (Public Procurement) - 2 tools",
+ "Rekabet Kurumu (Competition Authority) - 2 tools",
+ "Sayıştay (Court of Accounts) - 6 tools",
+ "Bedesten API Courts (Local/Appellate/KYB) - 6 tools"
+ ],
+ mcp_endpoint="/mcp-server/mcp/",
+ api_docs="/docs"
+ )
+
+@app.get("/health", response_model=HealthCheck)
+async def health_check():
+ """Health check with comprehensive system status"""
+ uptime = (datetime.now() - SERVER_START_TIME).total_seconds()
+ return HealthCheck(
+ status="healthy",
+ timestamp=datetime.now(),
+ uptime_seconds=uptime,
+ tools_operational=len(mcp_server._tool_manager._tools) == 33
+ )
+
+@app.get("/api/tools", response_model=List[ToolInfo])
+async def list_tools(
+ search: Optional[str] = Query(None, description="Search tools by name or description"),
+ database: Optional[str] = Query(None, description="Filter by database name")
+):
+ """List all 33 MCP tools with filtering capabilities"""
+ tools = []
+ for tool in mcp_server._tool_manager._tools.values():
+ if search and search.lower() not in tool.name.lower() and search.lower() not in tool.description.lower():
+ continue
+ if database:
+ db_lower = database.lower()
+ if db_lower not in tool.name.lower() and db_lower not in tool.description.lower():
+ continue
+
+ params = {}
+ if hasattr(tool, 'schema') and tool.schema:
+ if hasattr(tool.schema, 'parameters'):
+ params = tool.schema.parameters
+ elif hasattr(tool.schema, '__annotations__'):
+ params = {k: str(v) for k, v in tool.schema.__annotations__.items()}
+
+ tools.append(ToolInfo(
+ name=tool.name,
+ description=tool.description,
+ parameters=params
+ ))
+ return tools
+
+# ============================================================================
+# YARGITAY (COURT OF CASSATION) ENDPOINTS - 4 TOOLS
+# ============================================================================
+
+@app.post(
+ "/api/yargitay/search",
+ tags=["Yargıtay"],
+ summary="Search Court of Cassation (Primary API)",
+ description="""Search Turkey's Supreme Court for civil and criminal precedents using advanced operators.
+
+Key Features:
+• Advanced search: AND (+), OR (space), NOT (-), wildcards (*), exact phrases ("")
+• 52 chamber options (23 Civil + 23 Criminal + General Assemblies)
+• Date range filtering • Case/decision number filtering • Pagination
+
+Search Examples:
+• OR search: property share (finds ANY words)
+• Exact phrase: "property share" (finds exact phrase)
+• AND required: +"property share" +"annulment reason"
+• Wildcard: construct* (construction, constructive, etc.)
+• Exclude terms: +"property share" -"construction contract"
+
+Use for supreme court precedent research and legal principle analysis."""
+)
+async def search_yargitay(request: YargitaySearchRequest):
+ """
+ Searches Court of Cassation (Yargıtay) decisions using the primary official API.
+
+ The Court of Cassation (Yargıtay) is Turkey's highest court for civil and criminal matters,
+ equivalent to a Supreme Court. This tool provides access to the most comprehensive database
+ of supreme court precedents with advanced search capabilities and filtering options.
+
+ Key Features:
+ • Advanced search operators (AND, OR, wildcards, exclusions)
+ • Chamber filtering: 52 options (23 Civil (Hukuk) + 23 Criminal (Ceza) + General Assemblies (Genel Kurullar))
+ • Date range filtering with DD.MM.YYYY format
+ • Case number filtering (Case No (Esas No) and Decision No (Karar No))
+ • Pagination support (1-100 results per page)
+ • Multiple sorting options (by case number, decision number, date)
+
+ SEARCH SYNTAX GUIDE:
+ • Words with spaces: OR search ("property share" finds ANY of the words)
+ • "Quotes": Exact phrase search ("property share" finds exact phrase)
+ • Plus sign (+): AND search (property+share requires both words)
+ • Asterisk (*): Wildcard (construct* matches variations)
+ • Minus sign (-): Exclude terms (avoid unwanted results)
+
+ Common Search Patterns:
+ • Simple OR: property share (finds ~523K results)
+ • Exact phrase: "property share" (finds ~22K results)
+ • Multiple required: +"property share" +"annulment reason (bozma sebebi)" (finds ~234 results)
+ • Wildcard expansion: construct* (matches construction, constructive, etc.)
+ • Exclude unwanted: +"property share" -"construction contract"
+
+ Use cases:
+ • Research supreme court precedents and legal principles
+ • Find decisions from specific chambers (Civil (Hukuk) vs Criminal (Ceza))
+ • Search for interpretations of specific legal concepts
+ • Analyze court reasoning on complex legal issues
+ • Track legal developments over time periods
+
+ Returns structured search results with decision metadata. Use get_yargitay_document_markdown()
+ to retrieve full decision texts for detailed analysis.
+ """
+ args = {
+ "arananKelime": request.arananKelime,
+ "birimYrgKurulDaire": request.birimYrgKurulDaire,
+ "pageSize": request.pageSize
+ }
+ if request.baslangicTarihi:
+ args["baslangicTarihi"] = request.baslangicTarihi
+ if request.bitisTarihi:
+ args["bitisTarihi"] = request.bitisTarihi
+ return await call_mcp_tool("search_yargitay_detailed", args)
+
+@app.post(
+ "/api/yargitay/search-bedesten",
+ tags=["Yargıtay"],
+ summary="Search Court of Cassation (Bedesten API)",
+ description="""Alternative Court of Cassation search with exact phrase matching and recent decisions.
+
+Key Features:
+• Exact phrase search: "\"legal term\"" for precise matching
+• Regular search: "legal term" for individual word matching
+• 52 chamber filtering options (same as primary API)
+• ISO 8601 date filtering • Recent decision coverage
+
+Use alongside primary search for comprehensive coverage. Exact phrase search provides
+higher precision with fewer false positives."""
+)
+async def search_yargitay_bedesten(request: YargitayBedestenSearchRequest):
+ """Search Court of Cassation using Bedesten API. Complements primary API for complete coverage."""
+ args = {"phrase": request.phrase, "pageSize": request.pageSize}
+ if request.birimAdi:
+ args["birimAdi"] = request.birimAdi
+ if request.kararTarihiStart:
+ args["kararTarihiStart"] = request.kararTarihiStart
+ if request.kararTarihiEnd:
+ args["kararTarihiEnd"] = request.kararTarihiEnd
+ return await call_mcp_tool("search_yargitay_bedesten", args)
+
+@app.get(
+ "/api/yargitay/document/{decision_id}",
+ tags=["Yargıtay"],
+ summary="Get Court of Cassation Document (Primary API)",
+ description="""Retrieve complete Court of Cassation decision in Markdown format.
+
+Content includes:
+• Complete legal reasoning and precedent analysis
+• Detailed examination of lower court decisions
+• Citations of laws, regulations, and prior cases
+• Final ruling with legal justification
+
+Perfect for detailed legal analysis, precedent research, and citation building."""
+)
+async def get_yargitay_document(decision_id: str):
+ """Get full Court of Cassation decision text in clean Markdown format."""
+ return await call_mcp_tool("get_yargitay_document_markdown", {"id": decision_id})
+
+@app.get(
+ "/api/yargitay/bedesten-document/{document_id}",
+ tags=["Yargıtay"],
+ summary="Get Court of Cassation Document (Bedesten API)",
+ description="""Retrieve Court of Cassation decision from Bedesten API in Markdown format.
+
+Features:
+• Supports both HTML and PDF source documents
+• Clean Markdown conversion with legal structure preserved
+• Removes technical artifacts for easy reading
+• Compatible with documentId from Bedesten search results"""
+)
+async def get_yargitay_bedesten_document(document_id: str):
+ """Get Court of Cassation document from Bedesten API in Markdown format."""
+ return await call_mcp_tool("get_yargitay_bedesten_document_markdown", {"documentId": document_id})
+
+# ============================================================================
+# DANISTAY (COUNCIL OF STATE) ENDPOINTS - 5 TOOLS
+# ============================================================================
+
+@app.post(
+ "/api/danistay/search-keyword",
+ tags=["Danıştay"],
+ summary="Search Council of State (Keyword Logic)",
+ description="""Search Turkey's highest administrative court using Boolean keyword logic.
+
+Boolean Operators:
+• AND keywords: ALL must be present (required terms)
+• OR keywords: ANY can be present (alternative terms)
+• NOT keywords: EXCLUDE if present (unwanted terms)
+
+Examples:
+• Administrative acts: andKelimeler=["idari işlem", "iptal"]
+• Permits/licenses: orKelimeler=["ruhsat", "izin", "lisans"]
+• Exclude tax cases: notKelimeler=["vergi"]
+
+Perfect for administrative law research and government action reviews."""
+)
+async def search_danistay_keyword(request: DanistayKeywordSearchRequest):
+ """
+ Searches Council of State (Danıştay) decisions using keyword-based logic.
+
+ The Council of State (Danıştay) is Turkey's highest administrative court, responsible for
+ reviewing administrative actions and providing administrative law precedents. This tool
+ provides flexible keyword-based searching with Boolean logic operators.
+
+ Key Features:
+ • Boolean logic operators: AND, OR, NOT combinations
+ • Multiple keyword lists for complex search strategies
+ • Pagination support (1-100 results per page)
+ • Administrative law focus (permits, licenses, public administration)
+ • Complement to search_danistay_detailed for comprehensive coverage
+
+ Keyword Logic:
+ • andKelimeler: ALL keywords must be present (AND logic)
+ • orKelimeler: ANY keyword can be present (OR logic)
+ • notAndKelimeler: EXCLUDE if ALL keywords present (NOT AND)
+ • notOrKelimeler: EXCLUDE if ANY keyword present (NOT OR)
+
+ Administrative Law Use Cases:
+ • Research administrative court precedents
+ • Find decisions on specific government agencies
+ • Search for rulings on permits (ruhsat) and licenses (izin)
+ • Analyze administrative procedure interpretations
+ • Study public administration legal principles
+
+ Examples:
+ • Simple AND: andKelimeler=["administrative act (idari işlem)", "annulment (iptal)"]
+ • OR search: orKelimeler=["permit (ruhsat)", "permission (izin)", "license (lisans)"]
+ • Complex: andKelimeler=["municipality (belediye)"], notOrKelimeler=["tax (vergi)"]
+
+ Returns structured search results. Use get_danistay_document_markdown() for full texts.
+ For comprehensive Council of State (Danıştay) research, also use search_danistay_detailed and search_danistay_bedesten.
+ """
+ args = {"andKelimeler": request.andKelimeler, "pageSize": request.pageSize}
+ if request.orKelimeler:
+ args["orKelimeler"] = request.orKelimeler
+ if request.notKelimeler:
+ args["notKelimeler"] = request.notKelimeler
+ return await call_mcp_tool("search_danistay_by_keyword", args)
+
+@app.post(
+ "/api/danistay/search-detailed",
+ tags=["Danıştay"],
+ summary="Search Council of State (Detailed Criteria)",
+ description="""Most comprehensive Council of State search with advanced filtering.
+
+Advanced Filtering:
+• Chamber targeting (1. Daire through 17. Daire, special councils)
+• Case/decision number ranges • Date range filtering
+• Legislation cross-referencing • Multiple sorting options
+
+Use for specialized administrative law research, chamber-specific decisions,
+and regulatory compliance analysis."""
+)
+async def search_danistay_detailed(request: DanistayDetailedSearchRequest):
+ """Search Council of State with comprehensive filtering for specialized administrative law research."""
+ args = {}
+ for field in ["daire", "baslangicTarihi", "bitisTarihi", "esas", "karar"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_danistay_detailed", args)
+
+@app.post(
+ "/api/danistay/search-bedesten",
+ tags=["Danıştay"],
+ summary="Search Council of State (Bedesten API)",
+ description="""Council of State search via Bedesten API with 27 chamber options and exact phrase search.
+
+Key Features:
+• 27 chamber options (Main Councils, 17 Chambers, Military courts)
+• Exact phrase search with double quotes for precision
+• ISO 8601 date filtering • Alternative data source
+
+Use with other Danıştay tools for complete administrative law coverage."""
+)
+async def search_danistay_bedesten(request: DanistayBedestenSearchRequest):
+ """Search Council of State via Bedesten API. Use with other Danıştay tools for complete coverage."""
+ args = {"phrase": request.phrase, "pageSize": request.pageSize}
+ for field in ["birimAdi", "kararTarihiStart", "kararTarihiEnd"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_danistay_bedesten", args)
+
+@app.get(
+ "/api/danistay/document/{decision_id}",
+ tags=["Danıştay"],
+ summary="Get Council of State Document (Primary API)",
+ description="""Retrieve complete administrative court decision in Markdown format.
+
+Content includes:
+• Complete administrative law reasoning and precedent analysis
+• Review of administrative actions and government decisions
+• Citations of administrative laws and regulations
+• Final administrative ruling with legal justification
+
+Essential for administrative law research and government compliance analysis."""
+)
+async def get_danistay_document(decision_id: str):
+ """Get full Council of State decision text in clean Markdown format."""
+ return await call_mcp_tool("get_danistay_document_markdown", {"id": decision_id})
+
+@app.get(
+ "/api/danistay/bedesten-document/{document_id}",
+ tags=["Danıştay"],
+ summary="Get Council of State Document (Bedesten API)",
+ description="""Retrieve Council of State decision from Bedesten API in Markdown format."""
+)
+async def get_danistay_bedesten_document(document_id: str):
+ """Get Council of State document from Bedesten API in Markdown format."""
+ return await call_mcp_tool("get_danistay_bedesten_document_markdown", {"documentId": document_id})
+
+# ============================================================================
+# BEDESTEN API COURTS (LOCAL/APPELLATE/KYB) - 6 TOOLS
+# ============================================================================
+
+@app.post(
+ "/api/yerel-hukuk/search",
+ tags=["Yerel Hukuk"],
+ summary="Search Local Civil Courts",
+ description="""Search first-instance civil court decisions using Bedesten API.
+
+Local Civil Courts handle:
+• Contract disputes • Property rights • Family law • Tort claims
+• Commercial disputes • Consumer protection
+
+Only available tool for local court decisions. Supports exact phrase search
+and date filtering for precise legal research."""
+)
+async def search_yerel_hukuk(request: BedestenSearchRequest):
+ """
+ Searches Yerel Hukuk Mahkemesi (Local Civil Court) decisions using Bedesten API.
+
+ This provides access to local court decisions that are not available through other APIs.
+ Currently the only available tool for searching Yerel Hukuk Mahkemesi decisions.
+ Local civil courts represent the first instance of civil litigation in Turkey.
+
+ Local Civil Courts handle:
+ • Contract disputes and commercial litigation
+ • Property rights and real estate disputes
+ • Family law matters (divorce, custody, inheritance)
+ • Tort claims and compensation cases
+ • Consumer protection issues
+ • Employment disputes
+
+ Returns structured search results with decision metadata. Use get_yerel_hukuk_bedesten_document_markdown()
+ to retrieve full decision texts for detailed analysis.
+ """
+ args = {"phrase": request.phrase, "pageSize": request.pageSize}
+ for field in ["kararTarihiStart", "kararTarihiEnd"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_yerel_hukuk_bedesten", args)
+
+@app.get("/api/yerel-hukuk/document/{document_id}", tags=["Yerel Hukuk"])
+async def get_yerel_hukuk_document(document_id: str):
+ """
+ Retrieves a Yerel Hukuk Mahkemesi decision document from Bedesten API and converts to Markdown.
+
+ This tool fetches complete local court decision texts using documentId from search results.
+ Perfect for detailed analysis of first-instance civil court rulings.
+
+ Supports both HTML and PDF content types, automatically converting to clean Markdown format.
+ Use documentId from search_yerel_hukuk_bedesten results.
+ """
+ return await call_mcp_tool("get_yerel_hukuk_bedesten_document_markdown", {"documentId": document_id})
+
+@app.post(
+ "/api/istinaf-hukuk/search",
+ tags=["İstinaf Hukuk"],
+ summary="Search Civil Courts of Appeals",
+ description="""Search intermediate appellate court decisions using Bedesten API.
+
+İstinaf Courts are intermediate appellate courts handling appeals from local civil courts
+before cases reach the Court of Cassation. Only available tool for İstinaf decisions."""
+)
+async def search_istinaf_hukuk(request: BedestenSearchRequest):
+ """
+ Searches İstinaf Hukuk Mahkemesi (Civil Court of Appeals) decisions using Bedesten API.
+
+ İstinaf courts are intermediate appellate courts in the Turkish judicial system that handle
+ appeals from local civil courts before cases reach Yargıtay (Court of Cassation).
+ This is the only available tool for accessing İstinaf Hukuk Mahkemesi decisions.
+
+ Key Features:
+ • Date range filtering with ISO 8601 format (YYYY-MM-DDTHH:MM:SS.000Z)
+ • Exact phrase search using double quotes: "\"legal term\""
+ • Regular search for individual keywords
+ • Pagination support (1-100 results per page)
+
+ Use cases:
+ • Research appellate court precedents
+ • Track appeals from specific lower courts
+ • Find decisions on specific legal issues at appellate level
+ • Analyze intermediate court reasoning before supreme court review
+
+ Returns structured data with decision metadata including dates, case numbers, and summaries.
+ Use get_istinaf_hukuk_bedesten_document_markdown() to retrieve full decision texts.
+ """
+ args = {"phrase": request.phrase, "pageSize": request.pageSize}
+ for field in ["kararTarihiStart", "kararTarihiEnd"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_istinaf_hukuk_bedesten", args)
+
+@app.get("/api/istinaf-hukuk/document/{document_id}", tags=["İstinaf Hukuk"])
+async def get_istinaf_hukuk_document(document_id: str):
+ """
+ Retrieves the full text of an İstinaf Hukuk Mahkemesi decision document in Markdown format.
+
+ This tool converts the original decision document (HTML or PDF) from Bedesten API
+ into clean, readable Markdown format suitable for analysis and processing.
+
+ Input Requirements:
+ • documentId: Use the ID from search_istinaf_hukuk_bedesten results
+ • Document ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text with proper formatting
+ • Preserves legal structure (headers, paragraphs, citations)
+ • Removes extraneous HTML/PDF artifacts
+
+ Use for:
+ • Reading full appellate court decision texts
+ • Legal analysis of İstinaf court reasoning
+ • Citation extraction and reference building
+ • Content analysis and summarization
+ """
+ return await call_mcp_tool("get_istinaf_hukuk_bedesten_document_markdown", {"documentId": document_id})
+
+@app.post(
+ "/api/kyb/search",
+ tags=["KYB"],
+ summary="Search Extraordinary Appeals (KYB)",
+ description="""Search Kanun Yararına Bozma (Extraordinary Appeal) decisions.
+
+KYB is an extraordinary legal remedy where the Public Prosecutor's Office requests
+review of finalized decisions in favor of law and defendants. Very rare but important
+legal precedents. Only available tool for KYB decisions."""
+)
+async def search_kyb(request: BedestenSearchRequest):
+ """
+ Searches Kanun Yararına Bozma (KYB - Extraordinary Appeal) decisions using Bedesten API.
+
+ KYB is an extraordinary legal remedy in the Turkish judicial system where the
+ Public Prosecutor's Office can request review of finalized decisions in favor of
+ the law and defendants. This is the only available tool for accessing KYB decisions.
+
+ Key Features:
+ • Date range filtering with ISO 8601 format (YYYY-MM-DDTHH:MM:SS.000Z)
+ • Exact phrase search using double quotes: "\"extraordinary appeal\""
+ • Regular search for individual keywords
+ • Pagination support (1-100 results per page)
+
+ Legal Significance:
+ • Extraordinary remedy beyond regular appeals
+ • Initiated by Public Prosecutor's Office
+ • Reviews finalized decisions for legal errors
+ • Can benefit defendants retroactively
+ • Rare but important legal precedents
+
+ Use cases:
+ • Research extraordinary appeal precedents
+ • Study prosecutorial challenges to final decisions
+ • Analyze legal errors in finalized cases
+ • Track KYB success rates and patterns
+
+ Returns structured data with decision metadata. Use get_kyb_bedesten_document_markdown()
+ to retrieve full decision texts for detailed analysis.
+ """
+ args = {"phrase": request.phrase, "pageSize": request.pageSize}
+ for field in ["kararTarihiStart", "kararTarihiEnd"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_kyb_bedesten", args)
+
+@app.get("/api/kyb/document/{document_id}", tags=["KYB"])
+async def get_kyb_document(document_id: str):
+ """
+ Retrieves the full text of a Kanun Yararına Bozma (KYB) decision document in Markdown format.
+
+ This tool converts the original extraordinary appeal decision document (HTML or PDF)
+ from Bedesten API into clean, readable Markdown format for analysis.
+
+ Input Requirements:
+ • documentId: Use the ID from search_kyb_bedesten results
+ • Document ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text with legal formatting preserved
+ • Structured content with headers and citations
+ • Removes technical artifacts from source documents
+
+ Special Value for KYB Documents:
+ • Contains rare extraordinary appeal reasoning
+ • Shows prosecutorial arguments for legal review
+ • Documents correction of finalized legal errors
+ • Provides precedent for similar extraordinary circumstances
+
+ Use for:
+ • Analyzing extraordinary appeal legal reasoning
+ • Understanding prosecutorial review criteria
+ • Research on legal error correction mechanisms
+ • Studying retroactive benefit applications
+ """
+ return await call_mcp_tool("get_kyb_bedesten_document_markdown", {"documentId": document_id})
+
+# ============================================================================
+# ADDITIONAL COURTS - 12 TOOLS
+# ============================================================================
+
+@app.post("/api/emsal/search", tags=["Emsal"], summary="Search UYAP Precedents")
+async def search_emsal(request: EmsalSearchRequest):
+ """
+ Searches for Precedent (Emsal) decisions using detailed criteria.
+
+ The Precedent (Emsal) database contains precedent decisions from various Turkish courts
+ integrated through the UYAP (National Judiciary Informatics System). This tool provides
+ access to a comprehensive collection of court decisions that serve as legal precedents.
+
+ Key Features:
+ • Multi-court coverage (BAM, Civil courts, Regional chambers)
+ • Keyword-based search across decision texts
+ • Court-specific filtering for targeted research
+ • Case number filtering (Case No (Esas No) and Decision No (Karar No) with ranges)
+ • Date range filtering with DD.MM.YYYY format
+ • Multiple sorting options and pagination support
+
+ Court Selection Options:
+ • BAM Civil Courts: Higher regional civil courts
+ • Civil Courts: Local and first-instance civil courts
+ • Regional Civil Chambers: Specialized civil court departments
+
+ Precedent Research Use Cases:
+ • Find precedent (emsal) decisions across multiple court levels
+ • Research court interpretations of specific legal concepts
+ • Analyze consistent legal reasoning patterns
+ • Study regional variations in legal decisions
+ • Track precedent development over time
+ • Compare decisions from different court types
+
+ Returns structured precedent data with court information and decision metadata.
+ Use get_emsal_document_markdown() to retrieve full precedent decision texts.
+ """
+ args = {"keyword": request.keyword, "results_per_page": request.results_per_page}
+ if request.decision_year_karar:
+ args["decision_year_karar"] = request.decision_year_karar
+ return await call_mcp_tool("search_emsal_detailed_decisions", args)
+
+@app.get("/api/emsal/document/{decision_id}", tags=["Emsal"])
+async def get_emsal_document(decision_id: str):
+ """
+ Retrieves the full text of a specific Emsal (UYAP Precedent) decision in Markdown format.
+
+ This tool fetches complete precedent decision documents from the UYAP system and converts
+ them to clean, readable Markdown format suitable for legal precedent analysis.
+
+ Input Requirements:
+ • decision_id: Decision ID from search_emsal_detailed_decisions results
+ • ID must be non-empty string from UYAP Emsal database
+
+ Output Format:
+ • Clean Markdown text with legal precedent structure preserved
+ • Organized sections: court info, case facts, legal reasoning, conclusion
+ • Proper formatting for legal citations and cross-references
+ • Removes technical artifacts from source documents
+
+ Precedent Decision Content:
+ • Complete court reasoning and legal analysis
+ • Detailed examination of legal principles applied
+ • Citation of relevant laws, regulations, and prior precedents
+ • Final ruling with precedent-setting reasoning
+ • Court-specific interpretations and legal standards
+
+ Use for legal precedent research, citation building, and comparative legal analysis.
+ """
+ return await call_mcp_tool("get_emsal_document_markdown", {"decision_id": decision_id})
+
+@app.post("/api/uyusmazlik/search", tags=["Uyuşmazlık"], summary="Search Jurisdictional Disputes")
+async def search_uyusmazlik(request: UyusmazlikSearchRequest):
+ """
+ Searches for Court of Jurisdictional Disputes (Uyuşmazlık Mahkemesi) decisions.
+
+ The Court of Jurisdictional Disputes (Uyuşmazlık Mahkemesi) resolves jurisdictional disputes between different court systems
+ in Turkey, determining which court has jurisdiction over specific cases. This specialized
+ court handles conflicts between civil, criminal, and administrative jurisdictions.
+
+ Key Features:
+ • Department filtering (Criminal, Civil, General Assembly decisions)
+ • Dispute type classification (Jurisdiction vs Judgment disputes)
+ • Decision outcome filtering (dispute resolution results)
+ • Case number and date range filtering
+ • Advanced text search with Boolean logic operators
+ • Official Gazette reference search
+
+ Dispute Types:
+ • Jurisdictional Disputes (Görev Uyuşmazlığı): Which court has authority
+ • Judgment Disputes (Hüküm Uyuşmazlığı): Conflicting final decisions
+
+ Departments:
+ • Criminal Section (Ceza Bölümü): Criminal section decisions
+ • Civil Section (Hukuk Bölümü): Civil section decisions
+ • General Assembly Decisions (Genel Kurul Kararları): General Assembly decisions
+
+ Use cases:
+ • Research jurisdictional precedents
+ • Understand court system boundaries
+ • Analyze dispute resolution patterns
+ • Study inter-court conflict resolution
+ • Legal procedure and jurisdiction research
+
+ Returns structured search results with dispute resolution information.
+ """
+ return await call_mcp_tool("search_uyusmazlik_decisions", {
+ "keywords": request.keywords,
+ "page_to_fetch": request.page_to_fetch
+ })
+
+@app.get("/api/uyusmazlik/document", tags=["Uyuşmazlık"])
+async def get_uyusmazlik_document(document_url: str):
+ """
+ Retrieves the full text of a specific Uyuşmazlık Mahkemesi decision from its URL in Markdown format.
+
+ This tool fetches complete jurisdictional dispute resolution decisions and converts them
+ to clean, readable Markdown format suitable for legal analysis of inter-court disputes.
+
+ Input Requirements:
+ • document_url: Full URL to the decision document from search_uyusmazlik_decisions results
+ • URL must be valid HttpUrl format from official Uyuşmazlık Mahkemesi database
+
+ Output Format:
+ • Clean Markdown text with jurisdictional dispute structure preserved
+ • Organized sections: dispute facts, jurisdictional analysis, resolution ruling
+ • Proper formatting for legal citations and court system references
+ • Removes technical artifacts from source documents
+
+ Jurisdictional Dispute Decision Content:
+ • Complete analysis of jurisdictional conflicts between court systems
+ • Detailed examination of applicable jurisdictional rules
+ • Citation of relevant procedural laws and court organization statutes
+ • Final resolution determining proper court jurisdiction
+ • Reasoning for jurisdictional boundaries and court authority
+
+ Use for understanding court system boundaries, analyzing jurisdictional precedents,
+ and legal practice guidance on proper court selection.
+ """
+ return await call_mcp_tool("get_uyusmazlik_document_markdown_from_url", {"document_url": document_url})
+
+@app.post("/api/anayasa/search-norm", tags=["Anayasa"], summary="Search Constitutional Court (Norm Control)")
+async def search_anayasa_norm(request: AnayasaNormSearchRequest):
+ """
+ Searches Constitutional Court (Anayasa Mahkemesi) norm control decisions with comprehensive filtering.
+
+ The Constitutional Court is Turkey's highest constitutional authority, responsible for judicial
+ review of laws, regulations, and constitutional amendments. Norm control (Norm Denetimi) is the
+ court's power to review the constitutionality of legal norms.
+
+ Key Features:
+ • Boolean keyword search (AND, OR, NOT logic)
+ • Constitutional period filtering (1961 vs 1982 Constitution)
+ • Case and decision number filtering
+ • Date range filtering for review and decision dates
+ • Application type classification (İptal, İtiraz, etc.)
+ • Applicant filtering (government entities, opposition parties)
+ • Official Gazette publication filtering
+ • Judicial opinion analysis (dissents, different reasoning)
+ • Court member and rapporteur filtering
+ • Norm type classification (laws, regulations, decrees)
+ • Review outcome filtering (constitutionality determinations)
+ • Constitutional basis article referencing
+
+ Constitutional Review Types:
+ • Abstract review: Ex ante constitutional control
+ • Concrete review: Constitutional questions during litigation
+ • Legislative review: Parliamentary acts and government decrees
+ • Regulatory review: Administrative regulations and bylaws
+
+ Use cases:
+ • Constitutional law research and analysis
+ • Legislative drafting constitutional compliance
+ • Academic constitutional law study
+ • Legal precedent analysis for constitutional questions
+ • Government policy constitutional assessment
+
+ Returns structured constitutional court data with comprehensive metadata.
+ """
+ args = {"keywords_all": request.keywords_all, "results_per_page": request.results_per_page}
+ for field in ["period", "application_type"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_anayasa_norm_denetimi_decisions", args)
+
+@app.get("/api/anayasa/norm-document", tags=["Anayasa"])
+async def get_anayasa_norm_document(document_url: str, page_number: int = 1):
+ """Get Constitutional Court norm control decision in paginated Markdown format."""
+ return await call_mcp_tool("get_anayasa_norm_denetimi_document_markdown", {
+ "document_url": document_url, "page_number": page_number
+ })
+
+@app.post("/api/anayasa/search-bireysel", tags=["Anayasa"], summary="Search Constitutional Court (Individual Applications)")
+async def search_anayasa_bireysel(request: AnayasaBireyselSearchRequest):
+ """
+ Search Constitutional Court individual application (Bireysel Başvuru) decisions for human rights violation reports with keyword filtering.
+
+ Individual applications allow citizens to petition the Constitutional Court directly for
+ violations of fundamental rights and freedoms. This tool generates decision search reports
+ that help identify relevant human rights violation cases.
+
+ Key Features:
+ • Keyword-based search with AND logic
+ • Human rights violation case identification
+ • Individual petition decision analysis
+ • Fundamental rights and freedoms research
+ • Pagination support for large result sets
+
+ Individual Application System:
+ • Direct citizen access to Constitutional Court
+ • Human rights and fundamental freedoms protection
+ • Alternative to European Court of Human Rights
+ • Domestic remedy for constitutional violations
+ • Individual justice and rights enforcement
+
+ Human Rights Categories:
+ • Right to life and personal liberty
+ • Right to fair trial and due process
+ • Freedom of expression and press
+ • Freedom of religion and conscience
+ • Property rights and economic freedoms
+ • Right to privacy and family life
+ • Political rights and democratic participation
+
+ Use cases:
+ • Human rights violation research
+ • Individual petition precedent analysis
+ • Constitutional rights interpretation study
+ • Legal remedies for rights violations
+ • Academic human rights law research
+ • Civil society and NGO legal research
+
+ Returns search report with case summaries and violation categories.
+ Use get_anayasa_bireysel_document for full decision texts.
+ """
+ return await call_mcp_tool("search_anayasa_bireysel_basvuru_report", {
+ "keywords": request.keywords, "page_to_fetch": request.page_to_fetch
+ })
+
+@app.get("/api/anayasa/bireysel-document", tags=["Anayasa"])
+async def get_anayasa_bireysel_document(document_url: str, page_number: int = 1):
+ """
+ Retrieve the full text of a Constitutional Court individual application decision in paginated Markdown format.
+
+ This tool fetches complete human rights violation decisions from individual applications
+ and converts them to clean, readable Markdown format. Content is paginated into
+ 5,000-character chunks for easier processing.
+
+ Input Requirements:
+ • document_url: URL path (e.g., /BB/YYYY/NNNN) from search results
+ • page_number: Page number for pagination (1-indexed, default: 1)
+
+ Output Format:
+ • Clean Markdown text with human rights case structure preserved
+ • Organized sections: applicant info, violation claims, court analysis, ruling
+ • Proper formatting for human rights law citations and references
+ • Paginated content with navigation information
+
+ Individual Application Decision Content:
+ • Complete human rights violation analysis
+ • Detailed examination of fundamental rights claims
+ • Citation of constitutional articles and international human rights law
+ • Final determination on rights violations with remedies
+ • Analysis of domestic court proceedings and their adequacy
+ • Individual remedy recommendations and compensation
+
+ Use for:
+ • Reading full human rights violation decisions
+ • Human rights law research and precedent analysis
+ • Understanding constitutional rights protection standards
+ • Individual petition strategy development
+ • Academic human rights and constitutional law study
+ • Civil society monitoring of rights violations
+ """
+ return await call_mcp_tool("get_anayasa_bireysel_basvuru_document_markdown", {
+ "document_url": document_url, "page_number": page_number
+ })
+
+@app.post("/api/kik/search", tags=["KİK"], summary="Search Public Procurement Authority")
+async def search_kik(request: KikSearchRequest):
+ """
+ Search Public Procurement Authority (Kamu İhale Kurulu - KIK) decisions with comprehensive filtering for public procurement law and administrative dispute research.
+
+ The Public Procurement Authority (KIK) is Turkey's procurement oversight body, responsible for
+ regulating public procurement processes, resolving procurement disputes, and issuing interpretive
+ decisions on public contracting laws. This tool provides access to official procurement decisions
+ and regulatory guidance.
+
+ Key Features:
+ • Decision type filtering (Disputes, Regulatory, Court decisions)
+ • Decision date range filtering for temporal analysis
+ • Applicant and procuring entity filtering
+ • Tender subject and content-based search
+ • Decision number and reference tracking
+ • Comprehensive metadata extraction
+
+ Public Procurement Decision Types:
+ • Uyuşmazlık Kararları: Dispute resolution decisions
+ • Düzenleyici Kararlar: Regulatory and interpretive decisions
+ • Mahkeme Kararları: Court decisions and judicial precedents
+
+ Procurement Law Areas:
+ • Tender procedure compliance and violations
+ • Bid evaluation and award criteria disputes
+ • Contractor qualification and eligibility
+ • Contract modification and scope changes
+ • Performance guarantees and penalty applications
+ • Public procurement ethics and transparency
+ • Emergency procurement and exceptional procedures
+
+ Use cases:
+ • Public procurement law research and compliance guidance
+ • Tender dispute resolution precedent analysis
+ • Government contracting risk assessment
+ • Procurement policy and regulatory interpretation
+ • Academic public administration and law study
+ • Legal strategy development for procurement disputes
+
+ Returns structured procurement authority data with comprehensive case metadata.
+ Use get_kik_document for full decision texts with detailed reasoning.
+ """
+ args = {}
+ for field in ["karar_tipi", "karar_metni", "basvuru_konusu_ihale", "karar_tarihi_baslangic"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_kik_decisions", args)
+
+@app.get("/api/kik/document/{decision_id}", tags=["KİK"])
+async def get_kik_document(decision_id: str):
+ """
+ Retrieve the full text of a Public Procurement Authority (KIK) decision in paginated Markdown format.
+
+ This tool fetches complete public procurement decisions and converts them from PDF to clean,
+ readable Markdown format. Content is paginated into manageable chunks for processing lengthy
+ procurement law decisions and regulatory interpretations.
+
+ Input Requirements:
+ • decision_id: KIK decision ID (base64 encoded karar_id) from search results
+ • Decision ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text converted from original PDF documents
+ • Organized sections: case summary, legal analysis, regulatory interpretation, decision
+ • Proper formatting for procurement law citations and regulatory references
+ • Paginated content with navigation information
+ • Metadata including PDF source link and document information
+
+ Public Procurement Decision Content:
+ • Complete procurement dispute analysis and resolution
+ • Detailed examination of tender procedures and compliance
+ • Citation of procurement laws, regulations, and precedents
+ • Final determination on procurement violations with corrective measures
+ • Regulatory guidance and policy interpretations
+ • Contractor liability and penalty determinations
+
+ Use for:
+ • Reading full public procurement authority decisions
+ • Procurement law research and precedent analysis
+ • Government contracting compliance and risk assessment
+ • Tender dispute resolution strategy development
+ • Academic public administration and procurement law study
+ • Policy analysis and regulatory interpretation
+ """
+ return await call_mcp_tool("get_kik_document_markdown", {"decision_id": decision_id})
+
+@app.post("/api/rekabet/search", tags=["Rekabet"], summary="Search Competition Authority")
+async def search_rekabet(request: RekabetSearchRequest):
+ """
+ Search Competition Authority (Rekabet Kurumu) decisions with comprehensive filtering for competition law and antitrust research.
+
+ The Competition Authority (Rekabet Kurumu) is Turkey's competition authority, responsible for enforcing antitrust laws,
+ preventing anti-competitive practices, and regulating mergers and acquisitions. This tool
+ provides access to official competition law decisions and regulatory interpretations.
+
+ Key Features:
+ • Decision type filtering (Mergers, Violations, Exemptions, etc.)
+ • Title and content-based text search with exact phrase matching
+ • Publication date filtering
+ • Case year and decision number filtering
+ • Pagination support for large result sets
+
+ Competition Law Decision Types:
+ • Birleşme ve Devralma: Merger and acquisition approvals
+ • Rekabet İhlali: Competition violation investigations
+ • Muafiyet: Exemption and negative clearance decisions
+ • Geçici Tedbir: Interim measures and emergency orders
+ • Sektör İncelemesi: Sector inquiry and market studies
+ • Diğer: Other regulatory and interpretive decisions
+
+ Competition Law Areas:
+ • Anti-competitive agreements and cartels
+ • Abuse of dominant market position
+ • Merger control and market concentration
+ • Vertical agreements and distribution restrictions
+ • Unfair competition and consumer protection
+ • Market definition and economic analysis
+
+ Advanced Search:
+ • Exact phrase matching with double quotes for precise legal terms
+ • Content search across full decision texts (PdfText parameter)
+ • Title search for specific case names or topics
+ • Date range filtering for temporal analysis
+
+ Example for exact phrase search: PdfText=\"tender process\" consultancy
+
+ Use cases:
+ • Competition law research and precedent analysis
+ • Merger and acquisition due diligence
+ • Antitrust compliance and risk assessment
+ • Market analysis and competitive intelligence
+ • Academic competition economics study
+ • Legal strategy development for competition cases
+
+ Returns structured competition authority data with comprehensive metadata.
+ Use get_rekabet_document for full decision texts (paginated PDF conversion).
+ """
+ args = {"page": request.page}
+ for field in ["KararTuru", "PdfText", "YayinlanmaTarihi"]:
+ if getattr(request, field):
+ args[field] = getattr(request, field)
+ return await call_mcp_tool("search_rekabet_kurumu_decisions", args)
+
+@app.get("/api/rekabet/document/{karar_id}", tags=["Rekabet"])
+async def get_rekabet_document(karar_id: str, page_number: int = 1):
+ """
+ Retrieve the full text of a Competition Authority (Rekabet Kurumu) decision in paginated Markdown format converted from PDF.
+
+ This tool fetches complete competition authority decisions and converts them from PDF to clean,
+ readable Markdown format. Content is paginated for easier processing of lengthy competition law decisions.
+
+ Input Requirements:
+ • karar_id: GUID (kararId) from search_rekabet_kurumu_decisions results
+ • page_number: Page number for pagination (1-indexed, default: 1)
+
+ Output Format:
+ • Clean Markdown text converted from original PDF documents
+ • Organized sections: case summary, market analysis, legal reasoning, decision
+ • Proper formatting for competition law citations and references
+ • Paginated content with navigation information
+ • Metadata including PDF source link and document information
+
+ Competition Authority Decision Content:
+ • Complete competition law analysis and market assessment
+ • Detailed examination of anti-competitive practices
+ • Economic analysis and market definition studies
+ • Citation of competition laws, regulations, and precedents
+ • Final determination on competition violations with remedies
+ • Merger and acquisition approval conditions
+ • Regulatory guidance and policy interpretations
+
+ Use for:
+ • Reading full competition authority decisions
+ • Competition law research and precedent analysis
+ • Market analysis and economic impact assessment
+ • Antitrust compliance and risk evaluation
+ • Academic competition economics and law study
+ • Legal strategy development for competition cases
+ """
+ return await call_mcp_tool("get_rekabet_kurumu_document", {
+ "karar_id": karar_id, "page_number": page_number
+ })
+
+# ============================================================================
+# SAYISTAY (COURT OF ACCOUNTS) ENDPOINTS - 6 TOOLS
+# ============================================================================
+
+@app.post("/api/sayistay/search-genel-kurul", tags=["Sayıştay"], summary="Search Court of Accounts (General Assembly)")
+async def search_sayistay_genel_kurul(request: SayistaySearchRequest):
+ """
+ Search Sayıştay Genel Kurul (General Assembly) decisions - highest-level audit precedents and interpretive rulings with keyword-based filtering.
+
+ The General Assembly represents the highest decision-making body of Turkey's Court of Accounts,
+ issuing authoritative interpretations of audit laws, precedential rulings on complex accountability
+ issues, and binding guidance for audit practice. These decisions establish fundamental principles
+ for public financial oversight and accountability standards.
+
+ Key Features:
+ • Keyword-based search with AND logic for precise case finding
+ • Highest-level audit precedent identification
+ • Interpretive ruling analysis and legal guidance extraction
+ • Complex accountability issue resolution tracking
+ • Pagination support for comprehensive result sets
+
+ General Assembly Decision Authority:
+ • Ultimate audit law interpretation and clarification
+ • Precedential rulings binding on all audit chambers
+ • Complex inter-chamber jurisdiction dispute resolution
+ • Policy guidance for audit methodology and standards
+ • Final determination on constitutional audit questions
+
+ Public Audit Areas:
+ • Government budget execution and compliance
+ • Public institution financial management
+ • State-owned enterprise oversight and accountability
+ • Local government and municipal audit standards
+ • Public procurement oversight and compliance monitoring
+ • Performance audit methodology and effectiveness standards
+ • Public revenue collection and tax administration audit
+
+ Use Cases:
+ • Research authoritative audit law interpretations
+ • Find binding precedents for complex audit questions
+ • Study evolution of public accountability standards
+ • Analyze audit methodology development and refinement
+ • Academic public administration and audit research
+ • Government policy and accountability framework analysis
+
+ Returns structured General Assembly data with comprehensive legal precedent metadata.
+ Use get_sayistay_genel_kurul_document for full decision texts with detailed reasoning.
+ """
+ return await call_mcp_tool("search_sayistay_genel_kurul", {
+ "keywords": request.keywords, "page_to_fetch": request.page_to_fetch
+ })
+
+@app.get("/api/sayistay/genel-kurul-document", tags=["Sayıştay"])
+async def get_sayistay_genel_kurul_document(document_url: str, page_number: int = 1):
+ """
+ Retrieve the full text of a Sayıştay Genel Kurul decision document in Markdown format for detailed analysis.
+
+ This tool converts the original General Assembly decision document into clean,
+ readable Markdown format suitable for legal analysis and research.
+
+ Input Requirements:
+ • decision_id: Use the ID from search_sayistay_genel_kurul results
+ • Decision ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text with legal formatting preserved
+ • Structured content with reasoning and conclusions
+ • Removes technical artifacts from source documents
+
+ Use for:
+ • Detailed analysis of audit precedents
+ • Research on public accountability standards
+ • Citation and reference building
+ • Legal interpretation and case study development
+ """
+ return await call_mcp_tool("get_sayistay_genel_kurul_document_markdown", {
+ "document_url": document_url, "page_number": page_number
+ })
+
+@app.post("/api/sayistay/search-temyiz-kurulu", tags=["Sayıştay"], summary="Search Court of Accounts (Appeals Board)")
+async def search_sayistay_temyiz_kurulu(request: SayistaySearchRequest):
+ """
+ Search Sayıştay Temyiz Kurulu (Appeals Board) decisions - appellate review of audit chamber findings with advanced filtering and institutional analysis.
+
+ The Appeals Board serves as the intermediate appellate authority in Turkey's audit system,
+ reviewing first-instance chamber decisions on audit findings, liability determinations,
+ and sanctions. Appeals Board decisions refine audit standards and ensure consistency
+ across different audit chambers.
+
+ Key Features:
+ • Chamber-specific filtering for targeted appeals analysis
+ • Institutional type categorization for audit pattern analysis
+ • Decision and account year filtering for temporal trends
+ • Audit subject matter classification and content search
+ • Appeal outcome tracking and precedent identification
+ • Pagination support for comprehensive coverage
+
+ Appeals Board Authority:
+ • Review and modification of chamber audit findings
+ • Standardization of audit liability determinations
+ • Consistency enforcement across audit chambers
+ • Intermediate precedent development for audit practice
+ • Quality control for first-instance audit decisions
+
+ Audit Review Areas:
+ • Government agency financial accountability appeals
+ • Municipality and local government audit review
+ • State enterprise oversight and performance audit appeals
+ • Educational institution audit finding review
+ • Healthcare system financial accountability appeals
+ • Public procurement oversight and compliance review
+ • Tax administration and revenue audit appeals
+
+ Use Cases:
+ • Research audit appeals patterns and outcomes
+ • Study chamber decision consistency and standards
+ • Analyze audit liability determination evolution
+ • Find precedents for specific audit finding types
+ • Track institutional audit patterns and compliance
+ • Academic public accountability and audit law research
+
+ Returns structured Appeals Board data with comprehensive appellate analysis metadata.
+ Use get_sayistay_temyiz_kurulu_document for full appeals decisions with detailed reasoning.
+ """
+ return await call_mcp_tool("search_sayistay_temyiz_kurulu", {
+ "keywords": request.keywords, "page_to_fetch": request.page_to_fetch
+ })
+
+@app.get("/api/sayistay/temyiz-kurulu-document", tags=["Sayıştay"])
+async def get_sayistay_temyiz_kurulu_document(document_url: str, page_number: int = 1):
+ """
+ Retrieve the full text of a Sayıştay Temyiz Kurulu decision document in Markdown format for detailed appeals analysis.
+
+ This tool converts the original Appeals Board decision document into clean,
+ readable Markdown format for analysis of appellate reasoning and standards.
+
+ Input Requirements:
+ • decision_id: Use the ID from search_sayistay_temyiz_kurulu results
+ • Decision ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text with appellate reasoning preserved
+ • Structured content with original findings and appeals analysis
+ • Removes technical artifacts from source documents
+
+ Use for:
+ • Analysis of appeals board reasoning and standards
+ • Research on audit liability determination evolution
+ • Understanding chamber decision review criteria
+ • Precedent analysis for audit appeal cases
+ """
+ return await call_mcp_tool("get_sayistay_temyiz_kurulu_document_markdown", {
+ "document_url": document_url, "page_number": page_number
+ })
+
+@app.post("/api/sayistay/search-daire", tags=["Sayıştay"], summary="Search Court of Accounts (Chambers)")
+async def search_sayistay_daire(request: SayistaySearchRequest):
+ """
+ Search Sayıştay Daire (Chamber) decisions - first-instance audit findings and sanctions from individual audit chambers with comprehensive filtering and subject categorization.
+
+ Chamber decisions represent first-instance audit findings, sanctions, and
+ liability determinations issued by specialized audit chambers. These form
+ the foundation of Turkey's public financial accountability system.
+
+ Key Features:
+ • Chamber-specific filtering (8 specialized audit chambers)
+ • Decision and account year filtering (2012-2025)
+ • Public administration type categorization
+ • Subject matter classification and full-text search
+ • Audit report tracking and institutional analysis
+
+ Use Cases:
+ • Research specific audit findings and sanctions
+ • Study chamber specialization and jurisdiction
+ • Analyze audit patterns by institution type
+ • Find precedents for financial irregularities
+ • Track audit evolution across fiscal years
+ """
+ return await call_mcp_tool("search_sayistay_daire", {
+ "keywords": request.keywords, "page_to_fetch": request.page_to_fetch
+ })
+
+@app.get("/api/sayistay/daire-document", tags=["Sayıştay"])
+async def get_sayistay_daire_document(document_url: str, page_number: int = 1):
+ """
+ Retrieve the full text of a Sayıştay Daire decision document in Markdown format for detailed audit findings analysis.
+
+ This tool converts the original chamber decision document into clean,
+ readable Markdown format for analysis of first-instance audit findings.
+
+ Input Requirements:
+ • decision_id: Use the ID from search_sayistay_daire results
+ • Decision ID must be non-empty string
+
+ Output Format:
+ • Clean Markdown text with audit findings preserved
+ • Structured content with violations and sanctions
+ • Removes technical artifacts from source documents
+
+ Use for:
+ • Detailed analysis of audit findings and methodology
+ • Research on specific types of financial irregularities
+ • Understanding chamber jurisdiction and specialization
+ • Case study development for audit training and compliance
+ """
+ return await call_mcp_tool("get_sayistay_daire_document_markdown", {
+ "document_url": document_url, "page_number": page_number
+ })
+
+# ============================================================================
+# ADDITIONAL API ENDPOINTS
+# ============================================================================
+
+@app.get("/api/databases")
+async def list_databases():
+ """Comprehensive database information with tool mappings"""
+ return {
+ "total_tools": 33,
+ "databases": {
+ "yargitay": {
+ "name": "Yargıtay (Court of Cassation)",
+ "description": "Turkey's Supreme Court for civil and criminal matters",
+ "tools": 4,
+ "chambers": 52,
+ "search_tools": ["search_yargitay", "search_yargitay_bedesten"],
+ "document_tools": ["get_yargitay_document", "get_yargitay_bedesten_document"]
+ },
+ "danistay": {
+ "name": "Danıştay (Council of State)",
+ "description": "Turkey's Supreme Administrative Court",
+ "tools": 5,
+ "chambers": 27,
+ "search_tools": ["search_danistay_keyword", "search_danistay_detailed", "search_danistay_bedesten"],
+ "document_tools": ["get_danistay_document", "get_danistay_bedesten_document"]
+ },
+ "anayasa": {
+ "name": "Anayasa Mahkemesi (Constitutional Court)",
+ "description": "Constitutional review and individual applications",
+ "tools": 4,
+ "features": ["norm_control", "individual_applications", "human_rights"]
+ },
+ "rekabet": {
+ "name": "Rekabet Kurumu (Competition Authority)",
+ "description": "Antitrust and merger control",
+ "tools": 2,
+ "coverage": ["mergers", "cartels", "market_abuse", "sector_inquiries"]
+ },
+ "kik": {
+ "name": "Kamu İhale Kurulu (Public Procurement Authority)",
+ "description": "Government procurement disputes",
+ "tools": 2,
+ "coverage": ["procurement_disputes", "regulatory_decisions", "tender_violations"]
+ },
+ "sayistay": {
+ "name": "Sayıştay (Court of Accounts)",
+ "description": "Public audit and financial accountability",
+ "tools": 6,
+ "levels": ["general_assembly", "appeals_board", "audit_chambers"]
+ },
+ "emsal": {
+ "name": "Emsal (UYAP Precedents)",
+ "description": "Cross-court precedent database",
+ "tools": 2,
+ "coverage": ["multi_court", "precedent_analysis"]
+ },
+ "uyusmazlik": {
+ "name": "Uyuşmazlık Mahkemesi (Jurisdictional Disputes)",
+ "description": "Inter-court jurisdiction disputes",
+ "tools": 2,
+ "specialization": "jurisdictional_conflicts"
+ },
+ "bedesten_courts": {
+ "name": "Bedesten API Courts (Local/Appellate/KYB)",
+ "description": "First instance, appellate, and extraordinary appeal courts",
+ "tools": 6,
+ "courts": ["yerel_hukuk", "istinaf_hukuk", "kyb"],
+ "coverage": "complete_court_hierarchy"
+ }
+ }
+ }
+
+@app.get("/api/stats")
+async def get_statistics():
+ """Comprehensive API statistics and capabilities"""
+ uptime = (datetime.now() - SERVER_START_TIME).total_seconds()
+ return {
+ "server": {
+ "uptime_seconds": uptime,
+ "start_time": SERVER_START_TIME.isoformat(),
+ "version": "1.0.0",
+ "status": "operational"
+ },
+ "coverage": {
+ "total_tools": 33,
+ "total_databases": 9,
+ "total_chambers": 79, # 52 Yargıtay + 27 Danıştay
+ "search_tools": 16,
+ "document_tools": 17
+ },
+ "capabilities": {
+ "advanced_search_operators": True,
+ "exact_phrase_search": True,
+ "date_range_filtering": True,
+ "chamber_filtering": True,
+ "boolean_logic": True,
+ "wildcard_search": True,
+ "pagination": True,
+ "markdown_conversion": True,
+ "pdf_processing": True,
+ "dual_api_support": True
+ },
+ "legal_coverage": {
+ "supreme_courts": ["Yargıtay", "Danıştay"],
+ "constitutional_law": "Anayasa Mahkemesi",
+ "administrative_law": "Full coverage",
+ "competition_law": "Rekabet Kurumu",
+ "public_procurement": "KİK",
+ "public_audit": "Sayıştay",
+ "court_hierarchy": "Complete (Local → Appellate → Supreme)",
+ "specialized_courts": ["Uyuşmazlık", "Constitutional", "Administrative"]
+ }
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py
new file mode 100644
index 0000000..5a3d51a
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py
@@ -0,0 +1,1199 @@
+# kik_mcp_module/client.py
+import asyncio
+from playwright.async_api import (
+ async_playwright,
+ Page,
+ BrowserContext,
+ Browser,
+ Error as PlaywrightError,
+ TimeoutError as PlaywrightTimeoutError
+)
+from bs4 import BeautifulSoup
+import logging
+from typing import Dict, Any, List, Optional
+import urllib.parse
+import base64 # Base64 için
+import re
+import html as html_parser
+from markitdown import MarkItDown
+import os
+import math
+import io
+import random
+
+from .models import (
+ KikSearchRequest,
+ KikDecisionEntry,
+ KikSearchResult,
+ KikDocumentMarkdown,
+ KikKararTipi
+)
+
+logger = logging.getLogger(__name__)
+
+class KikApiClient:
+ BASE_URL = "https://ekap.kik.gov.tr"
+ SEARCH_PAGE_PATH = "/EKAP/Vatandas/kurulkararsorgu.aspx"
+ FIELD_LOCATORS = {
+ "karar_tipi_radio_group": "input[name='ctl00$ContentPlaceHolder1$kurulKararTip']",
+ "karar_no": "input[name='ctl00$ContentPlaceHolder1$txtKararNo']",
+ "karar_tarihi_baslangic": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBaslangic$EkapTakvimTextBox_etKararTarihBaslangic']",
+ "karar_tarihi_bitis": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBitis$EkapTakvimTextBox_etKararTarihBitis']",
+ "resmi_gazete_sayisi": "input[name='ctl00$ContentPlaceHolder1$txtResmiGazeteSayisi']",
+ "resmi_gazete_tarihi": "input[name='ctl00$ContentPlaceHolder1$etResmiGazeteTarihi$EkapTakvimTextBox_etResmiGazeteTarihi']",
+ "basvuru_konusu_ihale": "input[name='ctl00$ContentPlaceHolder1$txtBasvuruKonusuIhale']",
+ "basvuru_sahibi": "input[name='ctl00$ContentPlaceHolder1$txtSikayetci']",
+ "ihaleyi_yapan_idare": "input[name='ctl00$ContentPlaceHolder1$txtIhaleyiYapanIdare']",
+ "yil": "select[name='ctl00$ContentPlaceHolder1$ddlYil']",
+ "karar_metni": "input[name='ctl00$ContentPlaceHolder1$txtKararMetni']",
+ "search_button_id": "ctl00_ContentPlaceHolder1_btnAra"
+ }
+ RESULTS_TABLE_ID = "grdKurulKararSorguSonuc"
+ NO_RESULTS_MESSAGE_SELECTOR = "div#ctl00_MessageContent1"
+ VALIDATION_SUMMARY_SELECTOR = "div#ctl00_ValidationSummary1"
+ MODAL_CLOSE_BUTTON_SELECTOR = "div#detayPopUp.in a#btnKapatPencere_0.close"
+ DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
+
+ def __init__(self, request_timeout: float = 60000):
+ self.playwright_instance: Optional[async_playwright] = None
+ self.browser: Optional[Browser] = None
+ self.context: Optional[BrowserContext] = None
+ self.page: Optional[Page] = None
+ self.request_timeout = request_timeout
+ self._lock = asyncio.Lock()
+
+ async def _ensure_playwright_ready(self, force_new_page: bool = False):
+ async with self._lock:
+ browser_recreated = False
+ context_recreated = False
+ if not self.playwright_instance:
+ self.playwright_instance = await async_playwright().start()
+ if not self.browser or not self.browser.is_connected():
+ if self.browser: await self.browser.close()
+ # Ultra stealth browser configuration
+ self.browser = await self.playwright_instance.chromium.launch(
+ headless=True,
+ args=[
+ # Disable automation indicators
+ '--no-first-run',
+ '--no-default-browser-check',
+ '--disable-dev-shm-usage',
+ '--disable-extensions',
+ '--disable-gpu',
+ '--disable-default-apps',
+ '--disable-translate',
+ '--disable-blink-features=AutomationControlled',
+ '--disable-ipc-flooding-protection',
+ '--disable-renderer-backgrounding',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-client-side-phishing-detection',
+ '--disable-sync',
+ '--disable-features=TranslateUI,BlinkGenPropertyTrees',
+ '--disable-component-extensions-with-background-pages',
+ '--no-sandbox', # Sometimes needed for headless
+ '--disable-web-security',
+ '--disable-features=VizDisplayCompositor',
+ # Language and locale
+ '--lang=tr-TR',
+ '--accept-lang=tr-TR,tr;q=0.9,en;q=0.8',
+ # Performance optimizations
+ '--memory-pressure-off',
+ '--max_old_space_size=4096',
+ ]
+ )
+ browser_recreated = True
+ if not self.context or browser_recreated:
+ if self.context: await self.context.close()
+ if not self.browser: raise PlaywrightError("Browser not initialized.")
+ # Ultra realistic context configuration
+ self.context = await self.browser.new_context(
+ user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
+ viewport={'width': 1920, 'height': 1080},
+ screen={'width': 1920, 'height': 1080},
+ device_scale_factor=1.0,
+ is_mobile=False,
+ has_touch=False,
+ # Localization
+ locale='tr-TR',
+ timezone_id='Europe/Istanbul',
+ # Realistic browser features
+ java_script_enabled=True,
+ accept_downloads=True,
+ ignore_https_errors=True,
+ # Color scheme and media
+ color_scheme='light',
+ reduced_motion='no-preference',
+ forced_colors='none',
+ # Additional headers for realism
+ extra_http_headers={
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Accept-Language': 'tr-TR,tr;q=0.9,en;q=0.8',
+ 'Cache-Control': 'max-age=0',
+ 'DNT': '1',
+ 'Upgrade-Insecure-Requests': '1',
+ 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
+ 'Sec-Ch-Ua-Mobile': '?0',
+ 'Sec-Ch-Ua-Platform': '"Windows"',
+ 'Sec-Fetch-Dest': 'document',
+ 'Sec-Fetch-Mode': 'navigate',
+ 'Sec-Fetch-Site': 'none',
+ 'Sec-Fetch-User': '?1',
+ },
+ # Permissions to appear realistic
+ permissions=['geolocation'],
+ geolocation={'latitude': 41.0082, 'longitude': 28.9784}, # Istanbul
+ )
+ context_recreated = True
+ if not self.page or self.page.is_closed() or force_new_page or context_recreated or browser_recreated:
+ if self.page and not self.page.is_closed(): await self.page.close()
+ if not self.context: raise PlaywrightError("Context is None.")
+ self.page = await self.context.new_page()
+ if not self.page: raise PlaywrightError("Failed to create new page.")
+ self.page.set_default_navigation_timeout(self.request_timeout)
+ self.page.set_default_timeout(self.request_timeout)
+
+ # CRITICAL: Anti-detection JavaScript injection
+ await self._inject_stealth_scripts()
+ if not self.page or self.page.is_closed():
+ raise PlaywrightError("Playwright page initialization failed.")
+ logger.debug("_ensure_playwright_ready completed.")
+
+ async def close_client_session(self):
+ async with self._lock:
+ # ... (öncekiyle aynı)
+ if self.page and not self.page.is_closed(): await self.page.close(); self.page = None
+ if self.context: await self.context.close(); self.context = None
+ if self.browser: await self.browser.close(); self.browser = None
+ if self.playwright_instance: await self.playwright_instance.stop(); self.playwright_instance = None
+ logger.info("KikApiClient (Playwright): Resources closed.")
+
+ async def _inject_stealth_scripts(self):
+ """
+ Inject comprehensive stealth JavaScript to evade bot detection.
+ Overrides navigator properties and other fingerprinting vectors.
+ """
+ if not self.page:
+ logger.warning("Cannot inject stealth scripts: page is None")
+ return
+
+ logger.debug("Injecting comprehensive stealth scripts...")
+
+ stealth_script = '''
+ // Override navigator.webdriver
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => undefined,
+ configurable: true
+ });
+
+ // Override navigator properties to appear more human
+ Object.defineProperty(navigator, 'languages', {
+ get: () => ['tr-TR', 'tr', 'en-US', 'en'],
+ configurable: true
+ });
+
+ Object.defineProperty(navigator, 'platform', {
+ get: () => 'Win32',
+ configurable: true
+ });
+
+ Object.defineProperty(navigator, 'vendor', {
+ get: () => 'Google Inc.',
+ configurable: true
+ });
+
+ Object.defineProperty(navigator, 'deviceMemory', {
+ get: () => 8,
+ configurable: true
+ });
+
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
+ get: () => 8,
+ configurable: true
+ });
+
+ Object.defineProperty(navigator, 'maxTouchPoints', {
+ get: () => 0,
+ configurable: true
+ });
+
+ // Override plugins to appear realistic
+ Object.defineProperty(navigator, 'plugins', {
+ get: () => {
+ return [
+ {
+ 0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format", enabledPlugin: Plugin},
+ description: "Portable Document Format",
+ filename: "internal-pdf-viewer",
+ length: 1,
+ name: "Chrome PDF Plugin"
+ },
+ {
+ 0: {type: "application/pdf", suffixes: "pdf", description: "", enabledPlugin: Plugin},
+ description: "",
+ filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
+ length: 1,
+ name: "Chrome PDF Viewer"
+ }
+ ];
+ },
+ configurable: true
+ });
+
+ // Override permissions
+ const originalQuery = window.navigator.permissions.query;
+ window.navigator.permissions.query = (parameters) => (
+ parameters.name === 'notifications' ?
+ Promise.resolve({ state: Notification.permission }) :
+ originalQuery(parameters)
+ );
+
+ // Override WebGL rendering context
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
+ if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL
+ return 'Intel Inc.';
+ }
+ if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL
+ return 'Intel(R) Iris(R) Plus Graphics 640';
+ }
+ return getParameter(parameter);
+ };
+
+ // Override canvas fingerprinting
+ const toBlob = HTMLCanvasElement.prototype.toBlob;
+ const toDataURL = HTMLCanvasElement.prototype.toDataURL;
+ const getImageData = CanvasRenderingContext2D.prototype.getImageData;
+
+ const noisify = (canvas, context) => {
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+ for (let i = 0; i < imageData.data.length; i += 4) {
+ imageData.data[i] += Math.floor(Math.random() * 10) - 5;
+ imageData.data[i + 1] += Math.floor(Math.random() * 10) - 5;
+ imageData.data[i + 2] += Math.floor(Math.random() * 10) - 5;
+ }
+ context.putImageData(imageData, 0, 0);
+ };
+
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+ value: function(callback, type, encoderOptions) {
+ noisify(this, this.getContext('2d'));
+ return toBlob.apply(this, arguments);
+ }
+ });
+
+ Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', {
+ value: function(type, encoderOptions) {
+ noisify(this, this.getContext('2d'));
+ return toDataURL.apply(this, arguments);
+ }
+ });
+
+ // Override AudioContext for audio fingerprinting
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ const originalAnalyser = audioCtx.createAnalyser;
+ audioCtx.createAnalyser = function() {
+ const analyser = originalAnalyser.apply(this, arguments);
+ const getFloatFrequencyData = analyser.getFloatFrequencyData;
+ analyser.getFloatFrequencyData = function(array) {
+ getFloatFrequencyData.apply(this, arguments);
+ for (let i = 0; i < array.length; i++) {
+ array[i] += Math.random() * 0.0001;
+ }
+ };
+ return analyser;
+ };
+
+ // Override screen properties
+ Object.defineProperty(window.screen, 'colorDepth', {
+ get: () => 24,
+ configurable: true
+ });
+
+ Object.defineProperty(window.screen, 'pixelDepth', {
+ get: () => 24,
+ configurable: true
+ });
+
+ // Override timezone
+ Date.prototype.getTimezoneOffset = function() {
+ return -180; // UTC+3 (Istanbul)
+ };
+
+ // Override document.cookie to prevent tracking
+ const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
+ Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
+ if (originalCookieDescriptor && originalCookieDescriptor.configurable) {
+ Object.defineProperty(document, 'cookie', {
+ get: function() {
+ return originalCookieDescriptor.get.call(this);
+ },
+ set: function(val) {
+ console.log('Cookie set blocked:', val);
+ return originalCookieDescriptor.set.call(this, val);
+ },
+ configurable: true
+ });
+ }
+
+ // Remove automation traces
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON;
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object;
+ delete window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy;
+
+ // Add realistic performance timing
+ if (window.performance && window.performance.timing) {
+ const timing = window.performance.timing;
+ const now = Date.now();
+ Object.defineProperty(timing, 'navigationStart', { value: now - Math.floor(Math.random() * 1000) + 1000, configurable: false });
+ Object.defineProperty(timing, 'loadEventEnd', { value: now - Math.floor(Math.random() * 100) + 100, configurable: false });
+ }
+
+ console.log('✓ Stealth scripts injected successfully');
+ '''
+
+ try:
+ await self.page.add_init_script(stealth_script)
+ logger.debug("✅ Stealth scripts injected successfully")
+ except Exception as e:
+ logger.warning(f"⚠️ Failed to inject stealth scripts: {e}")
+
+ async def _simulate_human_behavior(self, fast_mode: bool = True):
+ """
+ Simulate realistic human behavior patterns to avoid detection.
+ Includes mouse movements, typing patterns, and natural delays.
+
+ Args:
+ fast_mode: If True, use minimal timing for speed optimization
+ """
+ if not self.page:
+ logger.warning("Cannot simulate human behavior: page is None")
+ return
+
+ logger.debug("🤖 Simulating human behavior patterns...")
+
+ try:
+ if fast_mode:
+ # ULTRA-FAST MODE: Minimal human behavior
+ viewport_size = self.page.viewport_size
+ if viewport_size and random.random() < 0.7: # 70% chance to do movement
+ width, height = viewport_size['width'], viewport_size['height']
+
+ # Single quick mouse movement
+ x = random.randint(200, width - 200)
+ y = random.randint(200, height - 200)
+ await self.page.mouse.move(x, y)
+
+ # Brief scroll (50% chance)
+ if random.random() < 0.5:
+ await self.page.mouse.wheel(0, random.randint(50, 100))
+
+ # Ultra-minimal delay
+ await asyncio.sleep(random.uniform(0.05, 0.15)) # Reduced from 0.1-0.3
+
+ else:
+ # FULL MODE: Original comprehensive behavior
+ viewport_size = self.page.viewport_size
+ if viewport_size:
+ width, height = viewport_size['width'], viewport_size['height']
+
+ # Generate 3-5 random mouse movements
+ movements = random.randint(3, 5)
+ logger.debug(f" 🖱️ Performing {movements} random mouse movements")
+
+ for i in range(movements):
+ x = random.randint(100, width - 100)
+ y = random.randint(100, height - 100)
+
+ # Move mouse with realistic speed (not instant)
+ await self.page.mouse.move(x, y)
+ await asyncio.sleep(random.uniform(0.1, 0.3))
+
+ # 2. Scroll simulation
+ logger.debug(" 📜 Simulating scroll behavior")
+ scroll_amount = random.randint(100, 300)
+ await self.page.mouse.wheel(0, scroll_amount)
+ await asyncio.sleep(random.uniform(0.2, 0.5))
+
+ # Scroll back up
+ await self.page.mouse.wheel(0, -scroll_amount)
+ await asyncio.sleep(random.uniform(0.2, 0.4))
+
+ # 3. Random page interaction delays
+ await asyncio.sleep(random.uniform(0.5, 1.5))
+
+ logger.debug("✅ Human behavior simulation completed")
+
+ except Exception as e:
+ logger.warning(f"⚠️ Human behavior simulation failed: {e}")
+
+ async def _human_type(self, selector: str, text: str, clear_first: bool = True, fast_mode: bool = True):
+ """
+ Type text with human-like patterns and delays.
+
+ Args:
+ selector: CSS selector for the input element
+ text: Text to type
+ clear_first: Whether to clear the field first
+ fast_mode: If True, use minimal delays for speed optimization
+ """
+ if not self.page:
+ logger.warning("Cannot perform human typing: page is None")
+ return
+
+ try:
+ if fast_mode:
+ # FAST MODE: Direct fill for speed
+ await self.page.fill(selector, text)
+ await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.1
+ else:
+ # FULL MODE: Character-by-character human typing
+ # Focus on the element first
+ await self.page.focus(selector)
+ await asyncio.sleep(random.uniform(0.1, 0.3))
+
+ # Clear field if requested
+ if clear_first:
+ await self.page.keyboard.press('Control+a')
+ await asyncio.sleep(random.uniform(0.05, 0.15))
+ await self.page.keyboard.press('Delete')
+ await asyncio.sleep(random.uniform(0.05, 0.15))
+
+ # Type each character with human-like delays
+ for char in text:
+ await self.page.keyboard.type(char)
+ # Human typing speed: 50-150ms between characters
+ delay = random.uniform(0.05, 0.15)
+
+ # Occasional longer pauses (thinking)
+ if random.random() < 0.1: # 10% chance
+ delay += random.uniform(0.2, 0.8)
+
+ await asyncio.sleep(delay)
+
+ # Brief pause after typing
+ await asyncio.sleep(random.uniform(0.2, 0.6))
+
+ logger.debug(f"✅ Human-typed '{text}' into {selector}")
+
+ except Exception as e:
+ logger.warning(f"⚠️ Human typing failed: {e}")
+
+ async def _human_click(self, selector: str, wait_before: bool = True, wait_after: bool = True, fast_mode: bool = True):
+ """
+ Perform a human-like click with realistic delays and mouse movement.
+
+ Args:
+ selector: CSS selector or element to click
+ wait_before: Whether to wait before clicking
+ wait_after: Whether to wait after clicking
+ fast_mode: If True, use minimal delays for speed optimization
+ """
+ if not self.page:
+ logger.warning("Cannot perform human click: page is None")
+ return
+
+ try:
+ if fast_mode:
+ # FAST MODE: Direct click with minimal delay
+ if wait_before:
+ await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15
+
+ await self.page.click(selector)
+
+ if wait_after:
+ await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15
+
+ else:
+ # FULL MODE: Realistic mouse movement and timing
+ # Wait before clicking (thinking time)
+ if wait_before:
+ await asyncio.sleep(random.uniform(0.3, 0.8))
+
+ # Get element bounds for realistic mouse movement
+ element = await self.page.query_selector(selector)
+ if element:
+ box = await element.bounding_box()
+ if box:
+ # Move to element with slight randomness
+ center_x = box['x'] + box['width'] / 2
+ center_y = box['y'] + box['height'] / 2
+
+ # Add small random offset
+ offset_x = random.uniform(-10, 10)
+ offset_y = random.uniform(-5, 5)
+
+ await self.page.mouse.move(center_x + offset_x, center_y + offset_y)
+ await asyncio.sleep(random.uniform(0.1, 0.3))
+
+ # Perform click
+ await self.page.mouse.click(center_x + offset_x, center_y + offset_y)
+
+ logger.debug(f"✅ Human-clicked {selector}")
+ else:
+ # Fallback to regular click
+ await self.page.click(selector)
+ logger.debug(f"✅ Fallback-clicked {selector}")
+ else:
+ logger.warning(f"⚠️ Element not found for human click: {selector}")
+ return
+
+ # Wait after clicking (processing time)
+ if wait_after:
+ await asyncio.sleep(random.uniform(0.2, 0.6))
+
+ logger.debug(f"✅ Human-clicked {selector}")
+
+ except Exception as e:
+ logger.warning(f"⚠️ Human click failed: {e}")
+
+ async def _simulate_page_exploration(self, fast_mode: bool = True):
+ """
+ Simulate natural page exploration before performing the main task.
+ This helps establish a more human-like session.
+
+ Args:
+ fast_mode: If True, use minimal exploration for speed optimization
+ """
+ if not self.page:
+ return
+
+ logger.debug("🕵️ Simulating page exploration...")
+
+ try:
+ if fast_mode:
+ # ULTRA-FAST MODE: Minimal exploration
+ await asyncio.sleep(random.uniform(0.05, 0.1)) # Reduced from 0.1-0.3
+
+ # Single mouse movement (optional)
+ try:
+ elements = await self.page.query_selector_all("input, button")
+ if elements and random.random() < 0.5: # 50% chance to skip
+ element = random.choice(elements)
+ box = await element.bounding_box()
+ if box:
+ center_x = box['x'] + box['width'] / 2
+ center_y = box['y'] + box['height'] / 2
+ await self.page.mouse.move(center_x, center_y)
+ except:
+ pass
+
+ await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.15
+
+ else:
+ # FULL MODE: Comprehensive exploration
+ # 1. Brief pause to "read" the page
+ await asyncio.sleep(random.uniform(1.0, 2.5))
+
+ # 2. Move mouse to various UI elements (like a human would explore)
+ explore_selectors = [
+ "h1", "h2", ".navbar", "#header", ".logo",
+ "input", "button", "a", ".form-group"
+ ]
+
+ explored = 0
+ for selector in explore_selectors:
+ elements = await self.page.query_selector_all(selector)
+ if elements and explored < 3: # Explore max 3 elements
+ element = random.choice(elements)
+ box = await element.bounding_box()
+ if box:
+ center_x = box['x'] + box['width'] / 2
+ center_y = box['y'] + box['height'] / 2
+
+ await self.page.mouse.move(center_x, center_y)
+ await asyncio.sleep(random.uniform(0.3, 0.8))
+ explored += 1
+
+ # 3. Small scroll to simulate reading
+ await self.page.mouse.wheel(0, random.randint(50, 150))
+ await asyncio.sleep(random.uniform(0.5, 1.2))
+
+ logger.debug("✅ Page exploration completed")
+
+ except Exception as e:
+ logger.debug(f"⚠️ Page exploration failed: {e}")
+
+ def _parse_decision_entries_from_soup(self, soup: BeautifulSoup, search_karar_tipi: KikKararTipi) -> List[KikDecisionEntry]:
+ entries: List[KikDecisionEntry] = []
+ table = soup.find("table", {"id": self.RESULTS_TABLE_ID})
+
+ logger.debug(f"Looking for table with ID: {self.RESULTS_TABLE_ID}")
+ if not table:
+ logger.warning(f"Table with ID '{self.RESULTS_TABLE_ID}' not found in HTML")
+ # Log available tables for debugging
+ all_tables = soup.find_all("table")
+ logger.debug(f"Found {len(all_tables)} tables in HTML")
+ for idx, tbl in enumerate(all_tables):
+ table_id = tbl.get('id', 'no-id')
+ table_class = tbl.get('class', 'no-class')
+ rows = tbl.find_all('tr')
+ logger.debug(f"Table {idx}: id='{table_id}', class='{table_class}', rows={len(rows)}")
+
+ # If this looks like a results table, try to use it
+ if (table_id and ('grd' in table_id.lower() or 'kurul' in table_id.lower() or 'sonuc' in table_id.lower())) or \
+ (isinstance(table_class, list) and any('grid' in cls.lower() or 'result' in cls.lower() for cls in table_class)) or \
+ len(rows) > 3: # Table with multiple rows might be results
+ logger.info(f"Trying to parse table {idx} as potential results table: id='{table_id}'")
+ table = tbl
+ break
+
+ if not table:
+ logger.error("No suitable results table found")
+ return entries
+
+ rows = table.find_all("tr")
+ logger.info(f"Found {len(rows)} rows in results table")
+
+ # Debug: Log first few rows structure
+ for i, row in enumerate(rows[:3]):
+ cells = row.find_all(["td", "th"])
+ cell_texts = [cell.get_text(strip=True)[:30] for cell in cells]
+ logger.info(f"Row {i} structure: {len(cells)} cells: {cell_texts}")
+
+ for row_idx, row in enumerate(rows):
+ # Skip first row (search bar with colspan=7) and second row (header with 6 cells)
+ if row_idx < 2:
+ logger.debug(f"Skipping header row {row_idx}")
+ continue
+
+ cells = row.find_all("td")
+ logger.debug(f"Row {row_idx}: Found {len(cells)} cells")
+
+ # Log cell contents for debugging
+ if cells and row_idx < 5: # Log first few data rows
+ for cell_idx, cell in enumerate(cells):
+ cell_text = cell.get_text(strip=True)[:50] # First 50 chars
+ logger.debug(f" Cell {cell_idx}: '{cell_text}...'")
+
+ # Be more flexible with cell count - try 6 cells first, then adapt
+ if len(cells) >= 5: # At least 5 cells for minimum required data
+ try:
+ # Try to find preview button in first cell or any cell with a link
+ preview_button_tag = None
+ event_target = ""
+
+ # Look for preview button in first few cells
+ for cell_idx in range(min(3, len(cells))):
+ cell = cells[cell_idx]
+ # Try multiple patterns for preview button (based on actual HTML structure)
+ preview_candidates = [
+ cell.find("a", id="btnOnizle"), # Exact match
+ cell.find("a", id=re.compile(r"btnOnizle$")),
+ cell.find("a", id=re.compile(r"btn.*Onizle")),
+ cell.find("a", id=re.compile(r".*Onizle.*")),
+ cell.find("a", href=re.compile(r"__doPostBack"))
+ ]
+
+ for candidate in preview_candidates:
+ if candidate and candidate.has_attr('href'):
+ match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", candidate['href'])
+ if match:
+ event_target = match.group(1)
+ preview_button_tag = candidate
+ logger.debug(f"Row {row_idx}: Found event_target '{event_target}' in cell {cell_idx}")
+ break
+
+ if preview_button_tag:
+ break
+
+ if not preview_button_tag:
+ logger.debug(f"Row {row_idx}: No preview button found in any cell")
+ # Log what links we found
+ for cell_idx, cell in enumerate(cells[:3]):
+ links_in_cell = cell.find_all("a")
+ logger.debug(f" Cell {cell_idx}: {len(links_in_cell)} links")
+ for link in links_in_cell[:2]:
+ logger.debug(f" Link id='{link.get('id')}', href='{link.get('href', '')[:50]}...'")
+
+ # Try to find decision data spans with more flexible patterns
+ karar_no_span = None
+ karar_tarihi_span = None
+ idare_span = None
+ basvuru_sahibi_span = None
+ ihale_span = None
+
+ # Try different span patterns for karar no (usually in cell 1)
+ for cell_idx in range(min(4, len(cells))):
+ if not karar_no_span:
+ cell = cells[cell_idx]
+ candidates = [
+ cell.find("span", id="lblKno"), # Exact match based on actual HTML
+ cell.find("span", id=re.compile(r"lblKno$")),
+ cell.find("span", id=re.compile(r".*Kno.*")),
+ cell.find("span", id=re.compile(r".*KararNo.*")),
+ cell.find("span", id=re.compile(r".*No.*"))
+ ]
+ for candidate in candidates:
+ if candidate and candidate.get_text(strip=True):
+ karar_no_span = candidate
+ logger.debug(f"Row {row_idx}: Found karar_no in cell {cell_idx}")
+ break
+
+ # Try different patterns for karar tarihi (usually in cell 2)
+ for cell_idx in range(min(4, len(cells))):
+ if not karar_tarihi_span:
+ cell = cells[cell_idx]
+ candidates = [
+ cell.find("span", id="lblKtar"), # Exact match based on actual HTML
+ cell.find("span", id=re.compile(r"lblKtar$")),
+ cell.find("span", id=re.compile(r".*Ktar.*")),
+ cell.find("span", id=re.compile(r".*Tarih.*")),
+ cell.find("span", id=re.compile(r".*Date.*"))
+ ]
+ for candidate in candidates:
+ if candidate and candidate.get_text(strip=True):
+ # Check if it looks like a date
+ text = candidate.get_text(strip=True)
+ if re.match(r'\d{1,2}[./]\d{1,2}[./]\d{4}', text):
+ karar_tarihi_span = candidate
+ logger.debug(f"Row {row_idx}: Found karar_tarihi in cell {cell_idx}")
+ break
+
+ # Find other spans in remaining cells (if we have 6 cells) - using exact IDs
+ if len(cells) >= 6:
+ idare_span = cells[3].find("span", id="lblIdare") or cells[3].find("span")
+ basvuru_sahibi_span = cells[4].find("span", id="lblSikayetci") or cells[4].find("span")
+ ihale_span = cells[5].find("span", id="lblIhale") or cells[5].find("span")
+ elif len(cells) == 5:
+ # Adjust for 5-cell layout
+ idare_span = cells[2].find("span") if cells[2] != cells[1] else None
+ basvuru_sahibi_span = cells[3].find("span") if len(cells) > 3 else None
+ ihale_span = cells[4].find("span") if len(cells) > 4 else None
+
+ # Log what we found
+ logger.debug(f"Row {row_idx}: karar_no_span={karar_no_span is not None}, "
+ f"karar_tarihi_span={karar_tarihi_span is not None}, "
+ f"event_target={bool(event_target)}")
+
+ # For KIK, we need at least karar_no and karar_tarihi, event_target is helpful but not critical
+ if not (karar_no_span and karar_tarihi_span):
+ logger.debug(f"Row {row_idx}: Missing required fields (karar_no or karar_tarihi), skipping")
+ # Log what spans we found in cells
+ for i, cell in enumerate(cells):
+ spans = cell.find_all("span")
+ if spans:
+ span_info = []
+ for s in spans:
+ span_id = s.get('id', 'no-id')
+ span_text = s.get_text(strip=True)[:20]
+ span_info.append(f"{span_id}:'{span_text}...'")
+ logger.debug(f" Cell {i} spans: {span_info}")
+ continue
+
+ # If we don't have event_target, we can still create an entry but mark it specially
+ if not event_target:
+ logger.warning(f"Row {row_idx}: No event_target found, document retrieval won't work")
+ event_target = f"missing_target_row_{row_idx}" # Placeholder
+
+ # Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor.
+ try:
+ entry = KikDecisionEntry(
+ preview_event_target=event_target,
+ kararNo=karar_no_span.get_text(strip=True),
+ karar_tipi=search_karar_tipi, # Arama yapılan karar tipini ekle
+ kararTarihi=karar_tarihi_span.get_text(strip=True),
+ idare=idare_span.get_text(strip=True) if idare_span else None,
+ basvuruSahibi=basvuru_sahibi_span.get_text(strip=True) if basvuru_sahibi_span else None,
+ ihaleKonusu=ihale_span.get_text(strip=True) if ihale_span else None,
+ )
+ entries.append(entry)
+ logger.info(f"Row {row_idx}: Successfully parsed decision: {entry.karar_no_str}")
+ except Exception as e:
+ logger.error(f"Row {row_idx}: Error creating KikDecisionEntry: {e}")
+ continue
+
+ except Exception as e:
+ logger.error(f"Error parsing row {row_idx}: {e}", exc_info=True)
+ else:
+ logger.warning(f"Row {row_idx}: Expected at least 5 cells but found {len(cells)}, skipping")
+ if len(cells) > 0:
+ cell_texts = [cell.get_text(strip=True)[:50] for cell in cells[:3]]
+ logger.debug(f"Row {row_idx} cells preview: {cell_texts}")
+
+ logger.info(f"Parsed {len(entries)} decision entries from {len(rows)} rows")
+ return entries
+
+ def _parse_total_records_from_soup(self, soup: BeautifulSoup) -> int:
+ # ... (öncekiyle aynı) ...
+ try:
+ pager_div = soup.find("div", class_="gridToplamSayi")
+ if pager_div:
+ match = re.search(r"Toplam Kayıt Sayısı:(\d+)", pager_div.get_text(strip=True))
+ if match: return int(match.group(1))
+ except: pass
+ return 0
+
+ def _parse_current_page_from_soup(self, soup: BeautifulSoup) -> int:
+ # ... (öncekiyle aynı) ...
+ try:
+ pager_div = soup.find("div", class_="sayfalama")
+ if pager_div:
+ active_page_span = pager_div.find("span", class_="active")
+ if active_page_span: return int(active_page_span.get_text(strip=True))
+ except: pass
+ return 1
+
+ async def search_decisions(self, search_params: KikSearchRequest) -> KikSearchResult:
+ await self._ensure_playwright_ready()
+ page = self.page
+ search_url = f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}"
+ try:
+ if page.url != search_url:
+ await page.goto(search_url, wait_until="networkidle", timeout=self.request_timeout)
+
+ # Simulate natural page exploration after navigation (FAST MODE)
+ await self._simulate_page_exploration(fast_mode=True)
+
+ search_button_selector = f"a[id='{self.FIELD_LOCATORS['search_button_id']}']"
+ await page.wait_for_selector(search_button_selector, state="visible", timeout=self.request_timeout)
+
+ current_karar_tipi_value = search_params.karar_tipi.value
+ radio_locator_selector = f"{self.FIELD_LOCATORS['karar_tipi_radio_group']}[value='{current_karar_tipi_value}']"
+ if not await page.locator(radio_locator_selector).is_checked():
+ js_target_radio = f"ctl00$ContentPlaceHolder1${current_karar_tipi_value}"
+ logger.info(f"Selecting radio button: {js_target_radio}")
+ async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
+ await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')")
+ # Ultra-fast wait for page to stabilize after radio button change
+ await page.wait_for_timeout(300) # Reduced from 1000ms
+ logger.info("Radio button selection completed")
+
+ # Helper function for human-like form filling (FAST MODE)
+ async def human_fill_if_value(selector_key: str, value: Optional[str]):
+ if value is not None:
+ selector = self.FIELD_LOCATORS[selector_key]
+ await self._human_type(selector, value, fast_mode=True)
+
+ # Karar No'yu KİK sitesine göndermeden önce '_' -> '/' dönüşümü yap
+ karar_no_for_kik_form = None
+ if search_params.karar_no: # search_params.karar_no Claude'dan '_' ile gelmiş olabilir
+ karar_no_for_kik_form = search_params.karar_no.replace('_', '/')
+ logger.info(f"Using karar_no '{karar_no_for_kik_form}' (transformed from '{search_params.karar_no}') for KIK form.")
+
+ # Fill form fields with FAST human-like behavior
+ logger.info("Filling form fields with fast mode...")
+
+ # Start with FAST mouse behavior simulation
+ await self._simulate_human_behavior(fast_mode=True)
+
+ await human_fill_if_value('karar_metni', search_params.karar_metni)
+ await human_fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan
+ await human_fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic)
+ await human_fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis)
+ await human_fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi)
+ await human_fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi)
+ await human_fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale)
+ await human_fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi)
+ await human_fill_if_value('ihaleyi_yapan_idare', search_params.ihaleyi_yapan_idare)
+
+ if search_params.yil:
+ await page.select_option(self.FIELD_LOCATORS['yil'], value=search_params.yil)
+ await page.wait_for_timeout(50) # Reduced from 100ms
+
+ logger.info("Form filling completed, preparing for search...")
+
+ # Additional FAST human behavior before search
+ await self._simulate_human_behavior(fast_mode=True)
+
+ action_is_search_button_click = (search_params.page == 1)
+ event_target_for_submit: str
+
+ try:
+ if action_is_search_button_click:
+ event_target_for_submit = self.FIELD_LOCATORS['search_button_id']
+ # Use human-like clicking for search button
+ search_button_selector = f"a[id='{event_target_for_submit}']"
+ logger.info(f"Performing human-like search button click...")
+
+ try:
+ # Hide datepicker first to prevent interference
+ await page.evaluate("$('#ui-datepicker-div').hide()")
+
+ # FAST Human-like click on search button
+ await self._human_click(search_button_selector, wait_before=True, wait_after=False, fast_mode=True)
+
+ # Wait for navigation
+ await page.wait_for_load_state("networkidle", timeout=self.request_timeout)
+ logger.info("Search navigation completed successfully")
+ except Exception as e:
+ logger.warning(f"Human click failed, falling back to JavaScript: {e}")
+ # Hide datepicker and use JavaScript fallback
+ await page.evaluate("$('#ui-datepicker-div').hide()")
+ async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
+ await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
+ logger.info("Search navigation completed via fallback")
+ else:
+ # Pagination - use original method for consistency
+ page_link_ctl_number = search_params.page + 2
+ event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}"
+ logger.info(f"Executing pagination with event target: {event_target_for_submit}")
+
+ async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
+ await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
+ logger.info("Pagination navigation completed successfully")
+ except PlaywrightTimeoutError:
+ logger.warning("Search navigation timed out, but continuing...")
+ await page.wait_for_timeout(5000) # Longer wait if navigation fails
+
+ # Ultra-fast wait time for results to load
+ logger.info("Waiting for search results to load...")
+ await page.wait_for_timeout(500) # Reduced from 1000ms
+
+ results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}"
+ try:
+ # First wait for any tables to appear (more general check)
+ logger.info("Waiting for any tables to appear...")
+ await page.wait_for_function("""
+ () => document.querySelectorAll('table').length > 0
+ """, timeout=4000) # Reduced from 8000ms
+ logger.info("At least one table appeared")
+
+ # Then wait for our specific table
+ await page.wait_for_selector(results_table_dom_selector, timeout=4000, state="attached") # Reduced from 8000ms
+ logger.debug("Results table attached to DOM")
+
+ # Wait for table to have some content (more than just headers)
+ await page.wait_for_function(f"""
+ () => {{
+ const table = document.querySelector('{results_table_dom_selector}');
+ return table && table.querySelectorAll('tr').length > 2;
+ }}
+ """, timeout=4000) # Reduced from 20000ms
+ logger.debug("Results table populated with data")
+
+ # Ultra-fast additional wait for any remaining JavaScript
+ await page.wait_for_timeout(500) # Reduced from 3000ms
+
+ except PlaywrightTimeoutError:
+ logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.")
+ # Try one more wait for content placeholder
+ try:
+ await page.wait_for_selector("#ctl00_ContentPlaceHolder1", timeout=10000)
+ logger.info("ContentPlaceHolder1 found, checking for tables...")
+ await page.wait_for_timeout(5000)
+ except PlaywrightTimeoutError:
+ logger.warning("ContentPlaceHolder1 also not found - content may not have loaded")
+
+ html_content = await page.content()
+ soup = BeautifulSoup(html_content, "html.parser")
+ # ... (hata ve sonuç yok mesajı kontrolü aynı) ...
+ validation_summary_tag = soup.find("div", id=self.VALIDATION_SUMMARY_SELECTOR.split('[')[0].split(':')[0])
+ if validation_summary_tag and validation_summary_tag.get_text(strip=True) and \
+ ("display: none" not in validation_summary_tag.get("style", "").lower() if validation_summary_tag.has_attr("style") else True) and \
+ validation_summary_tag.get_text(strip=True) != "":
+ return KikSearchResult(decisions=[], total_records=0, current_page=search_params.page)
+ message_content_div = soup.find("div", id=self.NO_RESULTS_MESSAGE_SELECTOR.split(':')[0])
+ if message_content_div and "kayıt bulunamamıştır" in message_content_div.get_text(strip=True).lower():
+ return KikSearchResult(decisions=[], total_records=0, current_page=1)
+
+ # _parse_decision_entries_from_soup'a arama yapılan karar_tipi'ni gönder
+ decisions = self._parse_decision_entries_from_soup(soup, search_params.karar_tipi)
+ total_records = self._parse_total_records_from_soup(soup)
+ current_page_from_html = self._parse_current_page_from_soup(soup)
+ return KikSearchResult(decisions=decisions, total_records=total_records, current_page=current_page_from_html)
+ except Exception as e:
+ logger.error(f"Error during KIK decision search: {e}", exc_info=True)
+ return KikSearchResult(decisions=[], current_page=search_params.page)
+
+ def _clean_html_for_markdown(self, html_content: str) -> str:
+ # ... (öncekiyle aynı) ...
+ if not html_content: return ""
+ return html_parser.unescape(html_content)
+
+ def _convert_html_to_markdown_internal(self, html_fragment: str) -> Optional[str]:
+ # ... (öncekiyle aynı) ...
+ if not html_fragment: return None
+ cleaned_html = self._clean_html_for_markdown(html_fragment)
+ markdown_output = None
+ try:
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = cleaned_html.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True)
+ markdown_output = md_converter.convert(html_stream).text_content
+ if markdown_output: markdown_output = re.sub(r'\n{3,}', '\n\n', markdown_output).strip()
+ except Exception as e: logger.error(f"MarkItDown conversion error: {e}", exc_info=True)
+ return markdown_output
+
+
+ async def get_decision_document_as_markdown(
+ self,
+ karar_id_b64: str,
+ page_number: int = 1
+ ) -> KikDocumentMarkdown:
+ await self._ensure_playwright_ready()
+ # Bu metodun kendi içinde yeni bir 'page' nesnesi ('doc_page_for_content') kullanacağını unutmayın,
+ # ana 'self.page' arama sonuçları sayfasında kalır.
+ current_main_page = self.page # Ana arama sonuçları sayfasını referans alalım
+
+ try:
+ decoded_key = base64.b64decode(karar_id_b64.encode('utf-8')).decode('utf-8')
+ karar_tipi_value, karar_no_for_search = decoded_key.split('|', 1)
+ original_karar_tipi = KikKararTipi(karar_tipi_value)
+ logger.info(f"KIK Get Detail: Decoded karar_id '{karar_id_b64}' to Karar Tipi: {original_karar_tipi.value}, Karar No: {karar_no_for_search}. Requested Markdown Page: {page_number}")
+ except Exception as e_decode:
+ logger.error(f"Invalid karar_id format. Could not decode Base64 or split: {karar_id_b64}. Error: {e_decode}")
+ return KikDocumentMarkdown(retrieved_with_karar_id=karar_id_b64, error_message="Invalid karar_id format.", current_page=page_number)
+
+ default_error_response_data = {
+ "retrieved_with_karar_id": karar_id_b64,
+ "retrieved_karar_no": karar_no_for_search,
+ "retrieved_karar_tipi": original_karar_tipi,
+ "error_message": "An unspecified error occurred.",
+ "current_page": page_number, "total_pages": 1, "is_paginated": False
+ }
+
+ # Ana arama sayfasında olduğumuzdan emin olalım
+ if self.SEARCH_PAGE_PATH not in current_main_page.url:
+ logger.info(f"Not on search page ({current_main_page.url}). Navigating to {self.SEARCH_PAGE_PATH} before targeted search for document.")
+ await current_main_page.goto(f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}", wait_until="networkidle", timeout=self.request_timeout)
+ await current_main_page.wait_for_selector(f"a[id='{self.FIELD_LOCATORS['search_button_id']}']", state="visible", timeout=self.request_timeout)
+
+ targeted_search_params = KikSearchRequest(
+ karar_no=karar_no_for_search,
+ karar_tipi=original_karar_tipi,
+ page=1
+ )
+ logger.info(f"Performing targeted search for Karar No: {karar_no_for_search}")
+ # search_decisions kendi içinde _ensure_playwright_ready çağırır ve self.page'i kullanır.
+ # Bu, current_main_page ile aynı olmalı.
+ search_results = await self.search_decisions(targeted_search_params)
+
+ if not search_results.decisions:
+ default_error_response_data["error_message"] = f"Decision with Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not found by internal search."
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ decision_to_fetch = None
+ for dec_entry in search_results.decisions:
+ if dec_entry.karar_no_str == karar_no_for_search and dec_entry.karar_tipi == original_karar_tipi:
+ decision_to_fetch = dec_entry
+ break
+
+ if not decision_to_fetch:
+ default_error_response_data["error_message"] = f"Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not present with an exact match in first page of targeted search results."
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ decision_preview_event_target = decision_to_fetch.preview_event_target
+ logger.info(f"Found target decision. Using preview_event_target: {decision_preview_event_target} for Karar No: {decision_to_fetch.karar_no_str}")
+
+ iframe_document_url_str = None
+ karar_id_param_from_url_on_doc_page = None
+ document_html_content = ""
+
+ try:
+ logger.info(f"Evaluating __doPostBack on main page to show modal for: {decision_preview_event_target}")
+ # Bu evaluate, self.page (yani current_main_page) üzerinde çalışır
+ await current_main_page.evaluate(f"javascript:__doPostBack('{decision_preview_event_target}','')")
+ await current_main_page.wait_for_timeout(1000)
+ logger.info(f"Executed __doPostBack for {decision_preview_event_target} on main page.")
+
+ iframe_selector = "iframe#iframe_detayPopUp"
+ modal_visible_selector = "div#detayPopUp.in"
+
+ try:
+ logger.info(f"Waiting for modal '{modal_visible_selector}' to be visible and iframe '{iframe_selector}' src to be populated on main page...")
+ await current_main_page.wait_for_function(
+ f"""
+ () => {{
+ const modal = document.querySelector('{modal_visible_selector}');
+ const iframe = document.querySelector('{iframe_selector}');
+ const modalIsTrulyVisible = modal && (window.getComputedStyle(modal).display !== 'none');
+ return modalIsTrulyVisible &&
+ iframe && iframe.getAttribute('src') &&
+ iframe.getAttribute('src').includes('KurulKararGoster.aspx');
+ }}
+ """,
+ timeout=self.request_timeout / 2
+ )
+ iframe_src_value = await current_main_page.locator(iframe_selector).get_attribute("src")
+ logger.info(f"Iframe src populated: {iframe_src_value}")
+
+ except PlaywrightTimeoutError:
+ logger.warning(f"Timeout waiting for KIK iframe src for {decision_preview_event_target}. Trying to parse from static content after presumed update.")
+ html_after_postback = await current_main_page.content()
+ # ... (fallback parsing öncekiyle aynı, default_error_response_data set edilir ve return edilir) ...
+ soup_after_postback = BeautifulSoup(html_after_postback, "html.parser")
+ detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "class": re.compile(r"\bin\b")})
+ if not detay_popup_div: detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "style": re.compile(r"display:\s*block", re.I)})
+ iframe_tag = detay_popup_div.find("iframe", {"id": "iframe_detayPopUp"}) if detay_popup_div else None
+ if iframe_tag and iframe_tag.has_attr("src") and iframe_tag["src"]: iframe_src_value = iframe_tag["src"]
+ else:
+ default_error_response_data["error_message"]="Timeout or failure finding decision content iframe URL after postback."
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ if not iframe_src_value or not iframe_src_value.strip():
+ default_error_response_data["error_message"]="Extracted iframe URL for decision content is empty."
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ # iframe_src_value göreceli bir URL ise, ana sayfanın URL'si ile birleştir
+ iframe_document_url_str = urllib.parse.urljoin(current_main_page.url, iframe_src_value)
+ logger.info(f"Constructed absolute iframe_document_url_str for goto: {iframe_document_url_str}") # Log this absolute URL
+ default_error_response_data["source_url"] = iframe_document_url_str
+
+ parsed_url = urllib.parse.urlparse(iframe_document_url_str)
+ query_params = urllib.parse.parse_qs(parsed_url.query)
+ karar_id_param_from_url_on_doc_page = query_params.get("KararId", [None])[0]
+ default_error_response_data["karar_id_param_from_url"] = karar_id_param_from_url_on_doc_page
+ if not karar_id_param_from_url_on_doc_page:
+ default_error_response_data["error_message"]="KararId (KIK internal ID) not found in extracted iframe URL."
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ logger.info(f"Fetching KIK decision content from iframe URL using a new Playwright page: {iframe_document_url_str}")
+
+ doc_page_for_content = await self.context.new_page()
+ try:
+ # `goto` metoduna MUTLAK URL verilmeli. Loglanan URL'nin mutlak olduğundan emin olalım.
+ await doc_page_for_content.goto(iframe_document_url_str, wait_until="domcontentloaded", timeout=self.request_timeout)
+ document_html_content = await doc_page_for_content.content()
+ except Exception as e_doc_page:
+ logger.error(f"Error navigating or getting content from doc_page ({iframe_document_url_str}): {e_doc_page}")
+ if doc_page_for_content and not doc_page_for_content.is_closed(): await doc_page_for_content.close()
+ default_error_response_data["error_message"]=f"Failed to load decision detail page: {e_doc_page}"
+ return KikDocumentMarkdown(**default_error_response_data)
+ finally:
+ if doc_page_for_content and not doc_page_for_content.is_closed():
+ await doc_page_for_content.close()
+
+ soup_decision_detail = BeautifulSoup(document_html_content, "html.parser")
+ karar_content_span = soup_decision_detail.find("span", {"id": "ctl00_ContentPlaceHolder1_lblKarar"})
+ actual_decision_html = karar_content_span.decode_contents() if karar_content_span else document_html_content
+ full_markdown_content = self._convert_html_to_markdown_internal(actual_decision_html)
+
+ if not full_markdown_content:
+ default_error_response_data["error_message"]="Markdown conversion failed or returned empty content."
+ try:
+ if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=1000):
+ await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
+ except: pass
+ return KikDocumentMarkdown(**default_error_response_data)
+
+ content_length = len(full_markdown_content); total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) or 1
+ current_page_clamped = max(1, min(page_number, total_pages))
+ start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ markdown_chunk = full_markdown_content[start_index : start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE]
+
+ try:
+ if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=2000):
+ await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
+ await current_main_page.wait_for_selector(f"div#detayPopUp:not(.in)", timeout=5000)
+ except: pass
+
+ return KikDocumentMarkdown(
+ retrieved_with_karar_id=karar_id_b64,
+ retrieved_karar_no=karar_no_for_search,
+ retrieved_karar_tipi=original_karar_tipi,
+ kararIdParam=karar_id_param_from_url_on_doc_page,
+ markdown_chunk=markdown_chunk, source_url=iframe_document_url_str,
+ current_page=current_page_clamped, total_pages=total_pages,
+ is_paginated=(total_pages > 1), full_content_char_count=content_length
+ )
+ except Exception as e:
+ logger.error(f"Error in get_decision_document_as_markdown for Karar ID {karar_id_b64}: {e}", exc_info=True)
+ default_error_response_data["error_message"] = f"General error: {str(e)}"
+ return KikDocumentMarkdown(**default_error_response_data)
diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py
new file mode 100644
index 0000000..6b6632b
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py
@@ -0,0 +1,74 @@
+# kik_mcp_module/models.py
+from pydantic import BaseModel, Field, HttpUrl, computed_field, ConfigDict
+from typing import List, Optional
+from enum import Enum
+import base64 # Base64 encoding/decoding için
+
+class KikKararTipi(str, Enum):
+ """Enum for KIK (Public Procurement Authority) Decision Types."""
+ UYUSMAZLIK = "rbUyusmazlik"
+ DUZENLEYICI = "rbDuzenleyici"
+ MAHKEME = "rbMahkeme"
+
+class KikSearchRequest(BaseModel):
+ """Model for KIK Decision search criteria."""
+ karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type")
+ karar_no: str = Field("", description="No")
+ karar_tarihi_baslangic: str = Field("", description="Start", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$")
+ karar_tarihi_bitis: str = Field("", description="End", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$")
+ resmi_gazete_sayisi: str = Field("", description="Gazette")
+ resmi_gazete_tarihi: str = Field("", description="Date", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$")
+ basvuru_konusu_ihale: str = Field("", description="Subject")
+ basvuru_sahibi: str = Field("", description="Applicant")
+ ihaleyi_yapan_idare: str = Field("", description="Entity")
+ yil: str = Field("", description="Year")
+ karar_metni: str = Field("", description="Text")
+ page: int = Field(1, ge=1, description="Page")
+
+class KikDecisionEntry(BaseModel):
+ """Represents a single decision entry from KIK search results."""
+ preview_event_target: str = Field(..., description="Event target")
+ karar_no_str: str = Field(..., alias="kararNo", description="Decision number")
+ karar_tipi: KikKararTipi = Field(..., description="Decision type")
+
+ karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date")
+ idare_str: str = Field("", alias="idare", description="Entity")
+ basvuru_sahibi_str: str = Field("", alias="basvuruSahibi", description="Applicant")
+ ihale_konusu_str: str = Field("", alias="ihaleKonusu", description="Subject")
+
+ @computed_field
+ @property
+ def karar_id(self) -> str:
+ """
+ A Base64 encoded unique ID for the decision, combining decision type and number.
+ Format before encoding: "{karar_tipi.value}|{karar_no_str}"
+ """
+ combined_key = f"{self.karar_tipi.value}|{self.karar_no_str}"
+ return base64.b64encode(combined_key.encode('utf-8')).decode('utf-8')
+
+ model_config = ConfigDict(populate_by_name=True)
+
+class KikSearchResult(BaseModel):
+ """Model for KIK search results."""
+ decisions: List[KikDecisionEntry]
+ total_records: int = 0
+ current_page: int = 1
+
+class KikDocumentMarkdown(BaseModel):
+ """
+ KIK decision document, with Markdown content potentially paginated.
+ """
+ retrieved_with_karar_id: Optional[str] = Field(None, description="Request ID")
+ retrieved_karar_no: Optional[str] = Field(None, description="Decision number")
+ retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="Decision type")
+
+ karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="Internal ID")
+ markdown_chunk: Optional[str] = Field(None, description="Content")
+ source_url: Optional[str] = Field(None, description="Source URL")
+ error_message: Optional[str] = Field(None, description="Error")
+ current_page: int = Field(1, description="Page")
+ total_pages: int = Field(1, description="Total pages")
+ is_paginated: bool = Field(False, description="Paginated")
+ full_content_char_count: Optional[int] = Field(None, description="Char count")
+
+ model_config = ConfigDict(populate_by_name=True)
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py
new file mode 100644
index 0000000..0409b6b
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py
@@ -0,0 +1 @@
+# kvkk_mcp_module/__init__.py
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py
new file mode 100644
index 0000000..ab26bb8
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py
@@ -0,0 +1,372 @@
+# kvkk_mcp_module/client.py
+
+import httpx
+from bs4 import BeautifulSoup
+from typing import List, Optional, Dict, Any
+import logging
+import os
+import re
+import io
+import math
+from urllib.parse import urljoin, urlparse, parse_qs
+from markitdown import MarkItDown
+from pydantic import HttpUrl
+
+from .models import (
+ KvkkSearchRequest,
+ KvkkDecisionSummary,
+ KvkkSearchResult,
+ KvkkDocumentMarkdown
+)
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers():
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+
+class KvkkApiClient:
+ """
+ API client for searching and retrieving KVKK (Personal Data Protection Authority) decisions
+ using Brave Search API for discovery and direct HTTP requests for content retrieval.
+ """
+
+ BRAVE_API_URL = "https://api.search.brave.com/res/v1/web/search"
+ KVKK_BASE_URL = "https://www.kvkk.gov.tr"
+ DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page
+
+ def __init__(self, request_timeout: float = 60.0):
+ """Initialize the KVKK API client."""
+ self.brave_api_token = os.getenv("BRAVE_API_TOKEN")
+ if not self.brave_api_token:
+ # Fallback to provided free token
+ self.brave_api_token = "BSAuaRKB-dvSDSQxIN0ft1p2k6N82Kq"
+ logger.info("Using fallback Brave API token (limited free token)")
+ else:
+ logger.info("Using Brave API token from environment variable")
+
+ self.http_client = httpx.AsyncClient(
+ headers={
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+ },
+ timeout=request_timeout,
+ verify=True,
+ follow_redirects=True
+ )
+
+ def _construct_search_query(self, keywords: str) -> str:
+ """Construct the search query for Brave API."""
+ base_query = 'site:kvkk.gov.tr "karar özeti"'
+ if keywords.strip():
+ return f"{base_query} {keywords.strip()}"
+ return base_query
+
+ def _extract_decision_id_from_url(self, url: str) -> Optional[str]:
+ """Extract decision ID from KVKK decision URL."""
+ try:
+ # Example URL: https://www.kvkk.gov.tr/Icerik/7288/2021-1303
+ parsed_url = urlparse(url)
+ path_parts = parsed_url.path.strip('/').split('/')
+
+ if len(path_parts) >= 3 and path_parts[0] == 'Icerik':
+ # Extract the decision ID from the path
+ decision_id = '/'.join(path_parts[1:]) # e.g., "7288/2021-1303"
+ return decision_id
+
+ except Exception as e:
+ logger.debug(f"Could not extract decision ID from URL {url}: {e}")
+
+ return None
+
+ def _extract_decision_metadata_from_title(self, title: str) -> Dict[str, Optional[str]]:
+ """Extract decision metadata from title string."""
+ metadata = {
+ "decision_date": None,
+ "decision_number": None
+ }
+
+ if not title:
+ return metadata
+
+ # Extract decision date (DD/MM/YYYY format)
+ date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', title)
+ if date_match:
+ metadata["decision_date"] = date_match.group(1)
+
+ # Extract decision number (YYYY/XXXX format)
+ number_match = re.search(r'(\d{4}/\d+)', title)
+ if number_match:
+ metadata["decision_number"] = number_match.group(1)
+
+ return metadata
+
+ async def search_decisions(self, params: KvkkSearchRequest) -> KvkkSearchResult:
+ """Search for KVKK decisions using Brave API."""
+
+ search_query = self._construct_search_query(params.keywords)
+ logger.info(f"KvkkApiClient: Searching with query: {search_query}")
+
+ try:
+ # Calculate offset for pagination
+ offset = (params.page - 1) * params.pageSize
+
+ response = await self.http_client.get(
+ self.BRAVE_API_URL,
+ headers={
+ "Accept": "application/json",
+ "Accept-Encoding": "gzip",
+ "x-subscription-token": self.brave_api_token
+ },
+ params={
+ "q": search_query,
+ "country": "TR",
+ "search_lang": "tr",
+ "ui_lang": "tr-TR",
+ "offset": offset,
+ "count": params.pageSize
+ }
+ )
+
+ response.raise_for_status()
+ data = response.json()
+
+ # Extract search results
+ decisions = []
+ web_results = data.get("web", {}).get("results", [])
+
+ for result in web_results:
+ title = result.get("title", "")
+ url = result.get("url", "")
+ description = result.get("description", "")
+
+ # Extract metadata from title
+ metadata = self._extract_decision_metadata_from_title(title)
+
+ # Extract decision ID from URL
+ decision_id = self._extract_decision_id_from_url(url)
+
+ decision = KvkkDecisionSummary(
+ title=title,
+ url=HttpUrl(url) if url else None,
+ description=description,
+ decision_id=decision_id,
+ publication_date=metadata.get("decision_date"),
+ decision_number=metadata.get("decision_number")
+ )
+ decisions.append(decision)
+
+ # Get total results if available
+ total_results = None
+ query_info = data.get("query", {})
+ if "total_results" in query_info:
+ total_results = query_info["total_results"]
+
+ return KvkkSearchResult(
+ decisions=decisions,
+ total_results=total_results,
+ page=params.page,
+ pageSize=params.pageSize,
+ query=search_query
+ )
+
+ except httpx.RequestError as e:
+ logger.error(f"KvkkApiClient: HTTP request error during search: {e}")
+ return KvkkSearchResult(
+ decisions=[],
+ total_results=0,
+ page=params.page,
+ pageSize=params.pageSize,
+ query=search_query
+ )
+ except Exception as e:
+ logger.error(f"KvkkApiClient: Unexpected error during search: {e}")
+ return KvkkSearchResult(
+ decisions=[],
+ total_results=0,
+ page=params.page,
+ pageSize=params.pageSize,
+ query=search_query
+ )
+
+ def _extract_decision_content_from_html(self, html: str, url: str) -> Dict[str, Any]:
+ """Extract decision content from KVKK decision page HTML."""
+ try:
+ soup = BeautifulSoup(html, 'html.parser')
+
+ # Extract title
+ title = None
+ title_element = soup.find('h3', class_='blog-post-title')
+ if title_element:
+ title = title_element.get_text(strip=True)
+ elif soup.title:
+ title = soup.title.get_text(strip=True)
+
+ # Extract decision content from the main content div
+ content_div = soup.find('div', class_='blog-post-inner')
+ if not content_div:
+ # Fallback to other possible content containers
+ content_div = soup.find('div', style='text-align:justify;')
+ if not content_div:
+ logger.warning(f"Could not find decision content div in {url}")
+ return {
+ "title": title,
+ "decision_date": None,
+ "decision_number": None,
+ "subject_summary": None,
+ "html_content": None
+ }
+
+ # Extract decision metadata from table
+ decision_date = None
+ decision_number = None
+ subject_summary = None
+
+ table = content_div.find('table')
+ if table:
+ rows = table.find_all('tr')
+ for row in rows:
+ cells = row.find_all('td')
+ if len(cells) >= 3:
+ field_name = cells[0].get_text(strip=True)
+ field_value = cells[2].get_text(strip=True)
+
+ if 'Karar Tarihi' in field_name:
+ decision_date = field_value
+ elif 'Karar No' in field_name:
+ decision_number = field_value
+ elif 'Konu Özeti' in field_name:
+ subject_summary = field_value
+
+ return {
+ "title": title,
+ "decision_date": decision_date,
+ "decision_number": decision_number,
+ "subject_summary": subject_summary,
+ "html_content": str(content_div)
+ }
+
+ except Exception as e:
+ logger.error(f"Error extracting content from HTML for {url}: {e}")
+ return {
+ "title": None,
+ "decision_date": None,
+ "decision_number": None,
+ "subject_summary": None,
+ "html_content": None
+ }
+
+ def _convert_html_to_markdown(self, html_content: str) -> Optional[str]:
+ """Convert HTML content to Markdown using MarkItDown with BytesIO to avoid filename length issues."""
+ if not html_content:
+ return None
+
+ try:
+ # Convert HTML string to bytes and create BytesIO stream
+ html_bytes = html_content.encode('utf-8')
+ html_stream = io.BytesIO(html_bytes)
+
+ # Pass BytesIO stream to MarkItDown to avoid temp file creation
+ md_converter = MarkItDown(enable_plugins=False)
+ result = md_converter.convert(html_stream)
+ return result.text_content
+ except Exception as e:
+ logger.error(f"Error converting HTML to Markdown: {e}")
+ return None
+
+ async def get_decision_document(self, decision_url: str, page_number: int = 1) -> KvkkDocumentMarkdown:
+ """Retrieve and convert a KVKK decision document to paginated Markdown."""
+ logger.info(f"KvkkApiClient: Getting decision document from: {decision_url}, page: {page_number}")
+
+ try:
+ # Fetch the decision page
+ response = await self.http_client.get(decision_url)
+ response.raise_for_status()
+
+ # Extract content from HTML
+ extracted_data = self._extract_decision_content_from_html(response.text, decision_url)
+
+ # Convert HTML content to Markdown
+ full_markdown_content = None
+ if extracted_data["html_content"]:
+ full_markdown_content = self._convert_html_to_markdown(extracted_data["html_content"])
+
+ if not full_markdown_content:
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=extracted_data["title"],
+ decision_date=extracted_data["decision_date"],
+ decision_number=extracted_data["decision_number"],
+ subject_summary=extracted_data["subject_summary"],
+ markdown_chunk=None,
+ current_page=page_number,
+ total_pages=0,
+ is_paginated=False,
+ error_message="Could not convert document content to Markdown"
+ )
+
+ # Calculate pagination
+ content_length = len(full_markdown_content)
+ total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE)
+ if total_pages == 0:
+ total_pages = 1
+
+ # Clamp page number to valid range
+ current_page_clamped = max(1, min(page_number, total_pages))
+
+ # Extract the requested chunk
+ start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE
+ markdown_chunk = full_markdown_content[start_index:end_index]
+
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=extracted_data["title"],
+ decision_date=extracted_data["decision_date"],
+ decision_number=extracted_data["decision_number"],
+ subject_summary=extracted_data["subject_summary"],
+ markdown_chunk=markdown_chunk,
+ current_page=current_page_clamped,
+ total_pages=total_pages,
+ is_paginated=(total_pages > 1),
+ error_message=None
+ )
+
+ except httpx.HTTPStatusError as e:
+ error_msg = f"HTTP error {e.response.status_code} when fetching decision document"
+ logger.error(f"KvkkApiClient: {error_msg}")
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=None,
+ decision_date=None,
+ decision_number=None,
+ subject_summary=None,
+ markdown_chunk=None,
+ current_page=page_number,
+ total_pages=0,
+ is_paginated=False,
+ error_message=error_msg
+ )
+ except Exception as e:
+ error_msg = f"Unexpected error when fetching decision document: {str(e)}"
+ logger.error(f"KvkkApiClient: {error_msg}")
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=None,
+ decision_date=None,
+ decision_number=None,
+ subject_summary=None,
+ markdown_chunk=None,
+ current_page=page_number,
+ total_pages=0,
+ is_paginated=False,
+ error_message=error_msg
+ )
+
+ async def close_client_session(self):
+ """Close the HTTP client session."""
+ if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed:
+ await self.http_client.aclose()
+ logger.info("KvkkApiClient: HTTP client session closed.")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py
new file mode 100644
index 0000000..7db1c3c
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py
@@ -0,0 +1,49 @@
+# kvkk_mcp_module/models.py
+
+from pydantic import BaseModel, Field, HttpUrl
+from typing import List, Optional, Any
+
+class KvkkSearchRequest(BaseModel):
+ """Model for KVKK (Personal Data Protection Authority) search request via Brave API."""
+ keywords: str = Field(..., description="""
+ Keywords to search for in KVKK decisions.
+ The search will automatically include 'site:kvkk.gov.tr "karar özeti"' to target KVKK decision summaries.
+ Examples: "açık rıza", "veri güvenliği", "kişisel veri işleme"
+ """)
+ page: int = Field(1, ge=1, le=50, description="Page number for search results (1-50).")
+ pageSize: int = Field(10, ge=1, le=10, description="Number of results per page (1-10).")
+
+class KvkkDecisionSummary(BaseModel):
+ """Model for a single KVKK decision summary from Brave search results."""
+ title: Optional[str] = Field(None, description="Decision title from search results.")
+ url: Optional[HttpUrl] = Field(None, description="URL to the KVKK decision page.")
+ description: Optional[str] = Field(None, description="Brief description or snippet from search results.")
+ decision_id: Optional[str] = Field(None, description="Value")
+ publication_date: Optional[str] = Field(None, description="Value")
+ decision_number: Optional[str] = Field(None, description="Value")
+
+class KvkkSearchResult(BaseModel):
+ """Model for the overall search result for KVKK decisions."""
+ decisions: List[KvkkDecisionSummary] = Field(default_factory=list, description="List of KVKK decisions found.")
+ total_results: Optional[int] = Field(None, description="Value")
+ page: int = Field(1, description="Current page number of results.")
+ pageSize: int = Field(10, description="Number of results per page.")
+ query: Optional[str] = Field(None, description="The actual search query sent to Brave API.")
+
+class KvkkDocumentMarkdown(BaseModel):
+ """Model for KVKK decision document content converted to paginated Markdown."""
+ source_url: HttpUrl = Field(description="URL of the original KVKK decision page.")
+ title: Optional[str] = Field(None, description="Title of the KVKK decision.")
+ decision_date: Optional[str] = Field(None, description="Decision date (Karar Tarihi).")
+ decision_number: Optional[str] = Field(None, description="Decision number (Karar No).")
+ subject_summary: Optional[str] = Field(None, description="Subject summary (Konu Özeti).")
+ markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content.")
+ current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).")
+ total_pages: int = Field(description="Total number of pages for the full markdown content.")
+ is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.")
+ error_message: Optional[str] = Field(None, description="Value")
+
+ class Config:
+ json_encoders = {
+ HttpUrl: str
+ }
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py
new file mode 100644
index 0000000..0ed22c9
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py
@@ -0,0 +1,28 @@
+"""
+MCP Auth Toolkit - OAuth 2.1 + Authorization for Model Context Protocol Servers
+Integrated with Clerk Authentication
+"""
+
+from .middleware import (
+ AuthContext,
+ FastMCPAuthWrapper,
+ MCPAuthMiddleware,
+ auth_required,
+)
+from .oauth import OAuthConfig, OAuthProvider
+from .policy import PolicyEngine, ToolPolicy, create_default_policies
+from .storage import PersistentStorage
+
+__version__ = "0.1.0"
+__all__ = [
+ "OAuthProvider",
+ "OAuthConfig",
+ "AuthContext",
+ "auth_required",
+ "create_default_policies",
+ "MCPAuthMiddleware",
+ "FastMCPAuthWrapper",
+ "PolicyEngine",
+ "ToolPolicy",
+ "PersistentStorage",
+]
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py
new file mode 100644
index 0000000..2c3e205
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py
@@ -0,0 +1,73 @@
+"""
+Clerk OAuth configuration for MCP Auth Toolkit
+"""
+
+import os
+import logging
+from .oauth import OAuthConfig
+
+logger = logging.getLogger(__name__)
+
+
+def create_clerk_oauth_config() -> OAuthConfig:
+ """Create OAuth configuration for Clerk integration using SDK"""
+
+ # Get Clerk configuration from environment
+ clerk_domain = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com")
+ clerk_publishable_key = os.getenv("CLERK_PUBLISHABLE_KEY")
+ clerk_secret_key = os.getenv("CLERK_SECRET_KEY")
+
+ if not clerk_publishable_key or not clerk_secret_key:
+ raise ValueError("CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY are required")
+
+ # For Clerk with custom domains, we use our adapter endpoints
+ # This allows us to handle the custom domain flow properly
+ base_url = os.getenv("BASE_URL", "https://yargimcp.com")
+
+ config = OAuthConfig(
+ client_id=clerk_publishable_key,
+ client_secret=clerk_secret_key,
+ # Use our adapter endpoints instead of Clerk's direct endpoints
+ authorization_endpoint=f"{base_url}/authorize",
+ token_endpoint=f"{base_url}/token",
+ # Keep Clerk's JWKS for token validation
+ jwks_uri=f"https://{clerk_domain}/.well-known/jwks.json",
+ issuer=base_url, # We're the issuer for MCP tokens
+ scopes=["mcp:tools:read", "mcp:tools:write", "openid", "profile", "email"]
+ )
+
+ logger.info(f"Created Clerk OAuth config with adapter endpoints")
+ logger.info(f"Clerk domain: {clerk_domain}")
+ logger.debug(f"Authorization endpoint: {config.authorization_endpoint}")
+ logger.debug(f"Token endpoint: {config.token_endpoint}")
+
+ return config
+
+
+def get_jwt_secret() -> str:
+ """Get JWT secret for token signing"""
+ jwt_secret = os.getenv("JWT_SECRET_KEY")
+
+ if not jwt_secret:
+ raise ValueError("JWT_SECRET_KEY environment variable is required")
+
+ return jwt_secret
+
+
+def create_mcp_server_config():
+ """Create complete MCP server configuration for Clerk integration"""
+
+ try:
+ oauth_config = create_clerk_oauth_config()
+ jwt_secret = get_jwt_secret()
+
+ return {
+ "oauth_config": oauth_config,
+ "jwt_secret": jwt_secret,
+ "base_url": os.getenv("BASE_URL", "https://yargi-mcp.fly.dev"),
+ "auth_enabled": os.getenv("ENABLE_AUTH", "true").lower() == "true"
+ }
+
+ except Exception as e:
+ logger.error(f"Failed to create MCP server config: {e}")
+ raise
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py
new file mode 100644
index 0000000..0dc6301
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py
@@ -0,0 +1,315 @@
+"""
+MCP server middleware for OAuth authentication and authorization
+"""
+
+import functools
+import logging
+from collections.abc import Callable
+from dataclasses import dataclass
+from typing import Any, Optional
+
+logger = logging.getLogger(__name__)
+
+try:
+ from fastmcp import FastMCP
+ FASTMCP_AVAILABLE = True
+except ImportError:
+ FASTMCP_AVAILABLE = False
+ FastMCP = None
+ logger.warning("FastMCP not available, some features will be disabled")
+
+from .oauth import OAuthProvider
+from .policy import PolicyEngine
+
+
+@dataclass
+class AuthContext:
+ """Authentication context passed to MCP tools"""
+
+ user_id: str
+ scopes: list[str]
+ claims: dict[str, Any]
+ token: str
+
+
+class MCPAuthMiddleware:
+ """Authentication middleware for MCP servers"""
+
+ def __init__(self, oauth_provider: OAuthProvider, policy_engine: PolicyEngine):
+ self.oauth_provider = oauth_provider
+ self.policy_engine = policy_engine
+
+ def authenticate_request(self, authorization_header: str) -> AuthContext | None:
+ """Extract and validate auth token from request"""
+
+ if not authorization_header:
+ logger.debug("No authorization header provided")
+ return None
+
+ if not authorization_header.startswith("Bearer "):
+ logger.debug("Authorization header does not start with 'Bearer '")
+ return None
+
+ token = authorization_header[7:] # Remove 'Bearer ' prefix
+
+ token_info = self.oauth_provider.introspect_token(token)
+
+ if not token_info.get("active"):
+ logger.warning("Token is not active")
+ return None
+
+ logger.debug(f"Authenticated user: {token_info.get('sub', 'unknown')}")
+
+ return AuthContext(
+ user_id=token_info.get("sub", "unknown"),
+ scopes=token_info.get("mcp_tool_scopes", []),
+ claims=token_info,
+ token=token,
+ )
+
+ def authorize_tool_call(
+ self, tool_name: str, auth_context: AuthContext
+ ) -> tuple[bool, str | None]:
+ """Check if user can call the specified tool"""
+
+ return self.policy_engine.authorize_tool_call(
+ tool_name=tool_name,
+ user_scopes=auth_context.scopes,
+ user_claims=auth_context.claims,
+ )
+
+
+def auth_required(
+ oauth_provider: OAuthProvider,
+ policy_engine: PolicyEngine,
+ tool_name: str | None = None,
+):
+ """
+ Decorator to require authentication for MCP tool functions
+
+ Usage:
+ @auth_required(oauth_provider, policy_engine, "search_yargitay")
+ def my_tool_function(context: AuthContext, ...):
+ pass
+ """
+
+ def decorator(func: Callable) -> Callable:
+ middleware = MCPAuthMiddleware(oauth_provider, policy_engine)
+
+ @functools.wraps(func)
+ async def wrapper(*args, **kwargs):
+ # Extract authorization header from kwargs
+ auth_header = kwargs.pop("authorization", None)
+
+ # Also check in args if it's a Request object
+ if not auth_header and args:
+ for arg in args:
+ if hasattr(arg, 'headers'):
+ auth_header = arg.headers.get("Authorization")
+ break
+
+ if not auth_header:
+ logger.warning(f"No authorization header for tool '{tool_name or func.__name__}'")
+ raise PermissionError("Authorization header required")
+
+ auth_context = middleware.authenticate_request(auth_header)
+
+ if not auth_context:
+ logger.warning(f"Authentication failed for tool '{tool_name or func.__name__}'")
+ raise PermissionError("Invalid or expired token")
+
+ actual_tool_name = tool_name or func.__name__
+
+ authorized, reason = middleware.authorize_tool_call(
+ actual_tool_name, auth_context
+ )
+
+ if not authorized:
+ logger.warning(f"Authorization failed for tool '{actual_tool_name}': {reason}")
+ raise PermissionError(f"Access denied: {reason}")
+
+ # Add auth context to function call
+ return await func(auth_context, *args, **kwargs)
+
+ return wrapper
+
+ return decorator
+
+
+class FastMCPAuthWrapper:
+ """Wrapper for FastMCP servers to add authentication"""
+
+ def __init__(
+ self,
+ mcp_server: "FastMCP",
+ oauth_provider: OAuthProvider,
+ policy_engine: PolicyEngine,
+ ):
+ if not FASTMCP_AVAILABLE:
+ raise ImportError("FastMCP is required for FastMCPAuthWrapper")
+
+ self.mcp_server = mcp_server
+ self.middleware = MCPAuthMiddleware(oauth_provider, policy_engine)
+ self.oauth_provider = oauth_provider
+ logger.info("Initializing FastMCP authentication wrapper")
+ self._wrap_tools()
+
+ def _wrap_tools(self):
+ """Wrap all existing tools with auth middleware"""
+
+ # Try different FastMCP tool storage locations
+ tool_registry = None
+
+ if hasattr(self.mcp_server, '_tools'):
+ tool_registry = self.mcp_server._tools
+ elif hasattr(self.mcp_server, 'tools'):
+ tool_registry = self.mcp_server.tools
+ elif hasattr(self.mcp_server, '_tool_registry'):
+ tool_registry = self.mcp_server._tool_registry
+ elif hasattr(self.mcp_server, '_handlers') and hasattr(self.mcp_server._handlers, 'tools'):
+ tool_registry = self.mcp_server._handlers.tools
+
+ if not tool_registry:
+ logger.warning("FastMCP server tool registry not found, tools will not be automatically wrapped")
+ logger.debug(f"Available server attributes: {dir(self.mcp_server)}")
+ return
+
+ logger.debug(f"Found tool registry with {len(tool_registry)} tools")
+ original_tools = dict(tool_registry)
+ wrapped_count = 0
+
+ for tool_name, tool_func in original_tools.items():
+ try:
+ wrapped_func = self._create_auth_wrapper(tool_name, tool_func)
+ tool_registry[tool_name] = wrapped_func
+ wrapped_count += 1
+ logger.debug(f"Wrapped tool: {tool_name}")
+ except Exception as e:
+ logger.error(f"Failed to wrap tool {tool_name}: {e}")
+
+ logger.info(f"Successfully wrapped {wrapped_count} tools with authentication")
+
+ def _create_auth_wrapper(self, tool_name: str, original_func: Callable) -> Callable:
+ """Create auth wrapper for a specific tool"""
+
+ @functools.wraps(original_func)
+ async def auth_wrapper(*args, **kwargs):
+ # Extract authorization from various sources
+ auth_header = None
+
+ # Check kwargs first
+ auth_header = kwargs.pop("authorization", None)
+
+ # Check if first argument is a Request object
+ if not auth_header and args:
+ first_arg = args[0]
+ if hasattr(first_arg, 'headers'):
+ auth_header = first_arg.headers.get("Authorization")
+
+ if not auth_header:
+ logger.warning(f"No authorization header for tool '{tool_name}'")
+ raise PermissionError("Authorization required")
+
+ auth_context = self.middleware.authenticate_request(auth_header)
+
+ if not auth_context:
+ logger.warning(f"Authentication failed for tool '{tool_name}'")
+ raise PermissionError("Invalid token")
+
+ authorized, reason = self.middleware.authorize_tool_call(
+ tool_name, auth_context
+ )
+
+ if not authorized:
+ logger.warning(f"Authorization failed for tool '{tool_name}': {reason}")
+ raise PermissionError(f"Access denied: {reason}")
+
+ # Add auth context to kwargs
+ kwargs["auth_context"] = auth_context
+ logger.debug(f"Calling tool '{tool_name}' for user {auth_context.user_id}")
+
+ return await original_func(*args, **kwargs)
+
+ return auth_wrapper
+
+ def add_oauth_endpoints(self):
+ """Add OAuth endpoints to the MCP server"""
+
+ @self.mcp_server.tool(
+ description="Initiate OAuth 2.1 authorization flow with PKCE",
+ annotations={"readOnlyHint": True, "idempotentHint": False}
+ )
+ async def oauth_authorize(redirect_uri: str, scopes: Optional[str] = None):
+ """OAuth authorization endpoint"""
+ scope_list = scopes.split(" ") if scopes else None
+ auth_url, pkce = self.oauth_provider.generate_authorization_url(
+ redirect_uri=redirect_uri, scopes=scope_list
+ )
+ logger.info(f"Generated authorization URL for redirect_uri: {redirect_uri}")
+ return {
+ "authorization_url": auth_url,
+ "code_verifier": pkce.verifier, # For PKCE flow
+ "code_challenge": pkce.challenge,
+ "instructions": "Use the authorization_url to complete OAuth flow, then exchange the returned code using oauth_token tool"
+ }
+
+ @self.mcp_server.tool(
+ description="Exchange OAuth authorization code for access token",
+ annotations={"readOnlyHint": False, "idempotentHint": False}
+ )
+ async def oauth_token(
+ code: str,
+ state: str,
+ redirect_uri: str
+ ):
+ """OAuth token exchange endpoint"""
+ try:
+ result = await self.oauth_provider.exchange_code_for_token(
+ code=code, state=state, redirect_uri=redirect_uri
+ )
+ logger.info("Successfully exchanged authorization code for token")
+ return result
+ except Exception as e:
+ logger.error(f"Token exchange failed: {e}")
+ raise
+
+ @self.mcp_server.tool(
+ description="Validate and introspect OAuth access token",
+ annotations={"readOnlyHint": True, "idempotentHint": True}
+ )
+ async def oauth_introspect(token: str):
+ """Token introspection endpoint"""
+ result = self.oauth_provider.introspect_token(token)
+ logger.debug(f"Token introspection: active={result.get('active', False)}")
+ return result
+
+ @self.mcp_server.tool(
+ description="Revoke OAuth access token",
+ annotations={"readOnlyHint": False, "idempotentHint": False}
+ )
+ async def oauth_revoke(token: str):
+ """Token revocation endpoint"""
+ success = self.oauth_provider.revoke_token(token)
+ logger.info(f"Token revocation: success={success}")
+ return {"revoked": success}
+
+ @self.mcp_server.tool(
+ description="Get list of tools available to authenticated user",
+ annotations={"readOnlyHint": True, "idempotentHint": True}
+ )
+ async def oauth_user_tools(authorization: str):
+ """Get user's allowed tools based on scopes"""
+ auth_context = self.middleware.authenticate_request(authorization)
+ if not auth_context:
+ raise PermissionError("Invalid token")
+
+ allowed_patterns = self.middleware.policy_engine.get_allowed_tools(auth_context.scopes)
+
+ return {
+ "user_id": auth_context.user_id,
+ "scopes": auth_context.scopes,
+ "allowed_tool_patterns": allowed_patterns,
+ "message": "Use these patterns to determine which tools you can access"
+ }
+
+ logger.info("Added OAuth endpoints: oauth_authorize, oauth_token, oauth_introspect, oauth_revoke, oauth_user_tools")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py
new file mode 100644
index 0000000..efc3adc
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py
@@ -0,0 +1,304 @@
+"""
+OAuth 2.1 + PKCE implementation for MCP servers with Clerk integration
+"""
+
+import base64
+import hashlib
+import secrets
+import time
+import logging
+from dataclasses import dataclass
+from datetime import datetime, timedelta
+from typing import Any, Optional
+from urllib.parse import urlencode
+
+import httpx
+import jwt
+from jwt.exceptions import PyJWTError, InvalidTokenError
+
+from .storage import PersistentStorage
+
+# Try to import Clerk SDK
+try:
+ from clerk_backend_api import Clerk
+ CLERK_AVAILABLE = True
+except ImportError:
+ CLERK_AVAILABLE = False
+ Clerk = None
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class OAuthConfig:
+ """OAuth provider configuration for Clerk"""
+
+ client_id: str
+ client_secret: str
+ authorization_endpoint: str
+ token_endpoint: str
+ jwks_uri: str | None = None
+ issuer: str = "mcp-auth"
+ scopes: list[str] = None
+
+ def __post_init__(self):
+ if self.scopes is None:
+ self.scopes = ["mcp:tools:read", "mcp:tools:write"]
+
+
+class PKCEChallenge:
+ """PKCE challenge/verifier pair for OAuth 2.1"""
+
+ def __init__(self):
+ self.verifier = (
+ base64.urlsafe_b64encode(secrets.token_bytes(32))
+ .decode("utf-8")
+ .rstrip("=")
+ )
+
+ challenge_bytes = hashlib.sha256(self.verifier.encode("utf-8")).digest()
+ self.challenge = (
+ base64.urlsafe_b64encode(challenge_bytes).decode("utf-8").rstrip("=")
+ )
+
+
+class OAuthProvider:
+ """OAuth 2.1 provider with PKCE support and Clerk integration"""
+
+ def __init__(self, config: OAuthConfig, jwt_secret: str):
+ self.config = config
+ self.jwt_secret = jwt_secret
+ # Use persistent storage instead of memory
+ self.storage = PersistentStorage()
+
+ # Initialize Clerk SDK if available
+ self.clerk = None
+ if CLERK_AVAILABLE and config.client_secret:
+ try:
+ self.clerk = Clerk(bearer_auth=config.client_secret)
+ logger.info("Clerk SDK initialized successfully")
+ except Exception as e:
+ logger.warning(f"Failed to initialize Clerk SDK: {e}")
+
+ logger.info("OAuth provider initialized with persistent storage")
+
+ def generate_authorization_url(
+ self,
+ redirect_uri: str,
+ state: str | None = None,
+ scopes: list[str] | None = None,
+ ) -> tuple[str, PKCEChallenge]:
+ """Generate OAuth authorization URL with PKCE for Clerk"""
+
+ pkce = PKCEChallenge()
+ session_id = secrets.token_urlsafe(32)
+
+ if state is None:
+ state = secrets.token_urlsafe(16)
+
+ if scopes is None:
+ scopes = self.config.scopes
+
+ # Store session data with expiration
+ session_data = {
+ "pkce_verifier": pkce.verifier,
+ "state": state,
+ "redirect_uri": redirect_uri,
+ "scopes": scopes,
+ "created_at": time.time(),
+ "expires_at": (datetime.utcnow() + timedelta(minutes=10)).timestamp(),
+ }
+ self.storage.set_session(session_id, session_data)
+
+ # Build Clerk OAuth URL
+ # Check if this is a custom domain (sign-in endpoint)
+ if self.config.authorization_endpoint.endswith('/sign-in'):
+ # For custom domains, Clerk expects redirect_url parameter
+ params = {
+ "redirect_url": redirect_uri,
+ "state": f"{state}:{session_id}",
+ }
+ auth_url = f"{self.config.authorization_endpoint}?{urlencode(params)}"
+ else:
+ # Standard OAuth flow with PKCE
+ params = {
+ "response_type": "code",
+ "client_id": self.config.client_id,
+ "redirect_uri": redirect_uri,
+ "scope": " ".join(scopes),
+ "state": f"{state}:{session_id}", # Combine state with session ID
+ "code_challenge": pkce.challenge,
+ "code_challenge_method": "S256",
+ }
+ auth_url = f"{self.config.authorization_endpoint}?{urlencode(params)}"
+
+ logger.info(f"Generated OAuth URL with session {session_id[:8]}...")
+ logger.debug(f"Auth URL: {auth_url}")
+ return auth_url, pkce
+
+ async def exchange_code_for_token(
+ self, code: str, state: str, redirect_uri: str
+ ) -> dict[str, Any]:
+ """Exchange authorization code for access token with Clerk"""
+
+ try:
+ original_state, session_id = state.split(":", 1)
+ except ValueError as e:
+ logger.error(f"Invalid state format: {state}")
+ raise ValueError("Invalid state format") from e
+
+ session = self.storage.get_session(session_id)
+ if not session:
+ logger.error(f"Session {session_id} not found")
+ raise ValueError("Invalid session")
+
+ # Check session expiration
+ if datetime.utcnow().timestamp() > session.get("expires_at", 0):
+ self.storage.delete_session(session_id)
+ logger.error(f"Session {session_id} expired")
+ raise ValueError("Session expired")
+
+ if session["state"] != original_state:
+ logger.error(f"State mismatch: expected {session['state']}, got {original_state}")
+ raise ValueError("State mismatch")
+
+ if session["redirect_uri"] != redirect_uri:
+ logger.error(f"Redirect URI mismatch: expected {session['redirect_uri']}, got {redirect_uri}")
+ raise ValueError("Redirect URI mismatch")
+
+ # Prepare token exchange request for Clerk
+ token_data = {
+ "grant_type": "authorization_code",
+ "client_id": self.config.client_id,
+ "client_secret": self.config.client_secret,
+ "code": code,
+ "redirect_uri": redirect_uri,
+ "code_verifier": session["pkce_verifier"],
+ }
+
+ logger.info(f"Exchanging code with Clerk for session {session_id[:8]}...")
+
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ self.config.token_endpoint,
+ data=token_data,
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
+ timeout=30.0,
+ )
+
+ if response.status_code != 200:
+ logger.error(f"Clerk token exchange failed: {response.status_code} - {response.text}")
+ raise ValueError(f"Token exchange failed: {response.text}")
+
+ token_response = response.json()
+ logger.info("Successfully exchanged code for Clerk token")
+
+ # Create MCP-scoped JWT token
+ access_token = self._create_mcp_token(
+ session["scopes"], token_response.get("access_token"), session_id
+ )
+
+ # Store token for introspection
+ token_id = secrets.token_urlsafe(16)
+ token_data = {
+ "access_token": access_token,
+ "scopes": session["scopes"],
+ "created_at": time.time(),
+ "expires_at": (datetime.utcnow() + timedelta(hours=1)).timestamp(),
+ "session_id": session_id,
+ "clerk_token": token_response.get("access_token"),
+ }
+ self.storage.set_token(token_id, token_data)
+
+ # Clean up session
+ self.storage.delete_session(session_id)
+
+ return {
+ "access_token": access_token,
+ "token_type": "bearer",
+ "expires_in": 3600,
+ "scope": " ".join(session["scopes"]),
+ }
+
+ def validate_pkce(self, code_verifier: str, code_challenge: str) -> bool:
+ """Validate PKCE code challenge (RFC 7636)"""
+ # S256 method
+ verifier_hash = hashlib.sha256(code_verifier.encode()).digest()
+ expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode().rstrip('=')
+ return expected_challenge == code_challenge
+
+ def _create_mcp_token(
+ self, scopes: list[str], upstream_token: str, session_id: str
+ ) -> str:
+ """Create MCP-scoped JWT token with Clerk token embedded"""
+
+ now = int(time.time())
+ payload = {
+ "iss": self.config.issuer,
+ "sub": session_id,
+ "aud": "mcp-server",
+ "iat": now,
+ "exp": now + 3600, # 1 hour expiration
+ "mcp_tool_scopes": scopes,
+ "upstream_token": upstream_token,
+ "clerk_integration": True,
+ }
+
+ return jwt.encode(payload, self.jwt_secret, algorithm="HS256")
+
+ def introspect_token(self, token: str) -> dict[str, Any]:
+ """Introspect and validate MCP token"""
+
+ try:
+ payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
+
+ # Check if token is expired
+ if payload.get("exp", 0) < time.time():
+ return {"active": False, "error": "token_expired"}
+
+ return {
+ "active": True,
+ "sub": payload.get("sub"),
+ "aud": payload.get("aud"),
+ "iss": payload.get("iss"),
+ "exp": payload.get("exp"),
+ "iat": payload.get("iat"),
+ "mcp_tool_scopes": payload.get("mcp_tool_scopes", []),
+ "upstream_token": payload.get("upstream_token"),
+ "clerk_integration": payload.get("clerk_integration", False),
+ }
+
+ except PyJWTError as e:
+ logger.warning(f"Token validation failed: {e}")
+ return {"active": False, "error": "invalid_token"}
+
+ def revoke_token(self, token: str) -> bool:
+ """Revoke a token"""
+
+ try:
+ payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
+ session_id = payload.get("sub")
+
+ # Remove all tokens associated with this session
+ all_tokens = self.storage.get_tokens()
+ tokens_to_remove = [
+ token_id
+ for token_id, token_data in all_tokens.items()
+ if token_data.get("session_id") == session_id
+ ]
+
+ for token_id in tokens_to_remove:
+ self.storage.delete_token(token_id)
+
+ logger.info(f"Revoked {len(tokens_to_remove)} tokens for session {session_id}")
+ return True
+
+ except InvalidTokenError as e:
+ logger.warning(f"Token revocation failed: {e}")
+ return False
+
+ def cleanup_expired_sessions(self):
+ """Clean up expired sessions and tokens"""
+ # This is now handled automatically by persistent storage
+ self.storage.cleanup_expired_sessions()
+ logger.debug("Cleanup completed via persistent storage")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py
new file mode 100644
index 0000000..e947af3
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py
@@ -0,0 +1,201 @@
+"""
+Authorization policy engine for MCP tools
+"""
+
+import re
+import logging
+from dataclasses import dataclass
+from enum import Enum
+from typing import Any
+
+logger = logging.getLogger(__name__)
+
+
+class PolicyAction(Enum):
+ ALLOW = "allow"
+ DENY = "deny"
+
+
+@dataclass
+class ToolPolicy:
+ """Policy rule for MCP tool access"""
+
+ tool_pattern: str # regex pattern for tool names
+ required_scopes: list[str]
+ action: PolicyAction = PolicyAction.ALLOW
+ conditions: dict[str, Any] | None = None
+
+ def matches_tool(self, tool_name: str) -> bool:
+ """Check if the policy applies to given tool"""
+ return bool(re.match(self.tool_pattern, tool_name))
+
+ def evaluate_scopes(self, user_scopes: list[str]) -> bool:
+ """Check if user has required scopes"""
+ return all(scope in user_scopes for scope in self.required_scopes)
+
+
+class PolicyEngine:
+ """Authorization policy engine for Turkish legal database tools"""
+
+ def __init__(self):
+ self.policies: list[ToolPolicy] = []
+ self.default_action = PolicyAction.DENY
+
+ def add_policy(self, policy: ToolPolicy):
+ """Add a policy rule"""
+ self.policies.append(policy)
+ logger.debug(f"Added policy: {policy.tool_pattern} -> {policy.required_scopes}")
+
+ def add_tool_scope_policy(
+ self,
+ tool_pattern: str,
+ required_scopes: str | list[str],
+ action: PolicyAction = PolicyAction.ALLOW,
+ ):
+ """Convenience method to add tool-scope policy"""
+ if isinstance(required_scopes, str):
+ required_scopes = [required_scopes]
+
+ policy = ToolPolicy(
+ tool_pattern=tool_pattern, required_scopes=required_scopes, action=action
+ )
+ self.add_policy(policy)
+
+ def authorize_tool_call(
+ self,
+ tool_name: str,
+ user_scopes: list[str],
+ user_claims: dict[str, Any] | None = None,
+ ) -> tuple[bool, str | None]:
+ """
+ Authorize a tool call
+
+ Returns:
+ (authorized: bool, reason: Optional[str])
+ """
+
+ logger.debug(f"Authorizing tool '{tool_name}' for user with scopes: {user_scopes}")
+
+ matching_policies = [
+ policy for policy in self.policies if policy.matches_tool(tool_name)
+ ]
+
+ if not matching_policies:
+ if self.default_action == PolicyAction.ALLOW:
+ logger.debug(f"No policies found for '{tool_name}', allowing by default")
+ return True, None
+ else:
+ logger.warning(f"No policies found for '{tool_name}', denying by default")
+ return False, f"No policy found for tool '{tool_name}', default deny"
+
+ # Check for explicit deny policies first
+ for policy in matching_policies:
+ if policy.action == PolicyAction.DENY:
+ if policy.evaluate_scopes(user_scopes):
+ logger.warning(f"Explicit deny policy matched for '{tool_name}'")
+ return False, f"Explicit deny policy for tool '{tool_name}'"
+
+ # Check allow policies
+ allow_policies = [
+ p for p in matching_policies if p.action == PolicyAction.ALLOW
+ ]
+
+ if not allow_policies:
+ logger.warning(f"No allow policies found for '{tool_name}'")
+ return False, f"No allow policies found for tool '{tool_name}'"
+
+ for policy in allow_policies:
+ if policy.evaluate_scopes(user_scopes):
+ if self._evaluate_conditions(policy.conditions, user_claims):
+ logger.debug(f"Authorization granted for '{tool_name}'")
+ return True, None
+
+ logger.warning(f"Insufficient scopes for '{tool_name}'. Required: {[p.required_scopes for p in allow_policies]}, User has: {user_scopes}")
+ return False, f"Insufficient scopes for tool '{tool_name}'"
+
+ def _evaluate_conditions(
+ self,
+ conditions: dict[str, Any] | None,
+ user_claims: dict[str, Any] | None,
+ ) -> bool:
+ """Evaluate additional policy conditions"""
+
+ if not conditions:
+ return True
+
+ if not user_claims:
+ logger.debug("No user claims provided, conditions evaluation failed")
+ return False
+
+ for key, expected_value in conditions.items():
+ user_value = user_claims.get(key)
+
+ if isinstance(expected_value, list):
+ if user_value not in expected_value:
+ logger.debug(f"Condition failed: {key} = {user_value} not in {expected_value}")
+ return False
+ elif user_value != expected_value:
+ logger.debug(f"Condition failed: {key} = {user_value} != {expected_value}")
+ return False
+
+ return True
+
+ def get_allowed_tools(self, user_scopes: list[str]) -> list[str]:
+ """Get list of tool patterns user is allowed to call"""
+
+ allowed_tools = []
+
+ for policy in self.policies:
+ if policy.action == PolicyAction.ALLOW and policy.evaluate_scopes(
+ user_scopes
+ ):
+ allowed_tools.append(policy.tool_pattern)
+
+ return allowed_tools
+
+
+def create_turkish_legal_policies() -> PolicyEngine:
+ """Create policy set for Turkish legal database MCP server"""
+
+ engine = PolicyEngine()
+
+ # Administrative tools (full access)
+ engine.add_tool_scope_policy(".*", ["mcp:tools:admin"])
+
+ # Search tools - require read access
+ engine.add_tool_scope_policy("search.*", ["mcp:tools:read"])
+
+ # Fetch/get document tools - require read access
+ engine.add_tool_scope_policy("get_.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("fetch.*", ["mcp:tools:read"])
+
+ # Specific Turkish legal database tools
+ engine.add_tool_scope_policy("search_yargitay.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_danistay.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_anayasa.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_rekabet.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_kik.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_emsal.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_uyusmazlik.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_sayistay.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_.*_bedesten", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_yerel_hukuk.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_istinaf_hukuk.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("search_kyb.*", ["mcp:tools:read"])
+
+ # Document retrieval tools
+ engine.add_tool_scope_policy("get_.*_document.*", ["mcp:tools:read"])
+ engine.add_tool_scope_policy("get_.*_markdown", ["mcp:tools:read"])
+
+ # Write operations (if any future tools need them)
+ engine.add_tool_scope_policy("create_.*", ["mcp:tools:write"])
+ engine.add_tool_scope_policy("update_.*", ["mcp:tools:write"])
+ engine.add_tool_scope_policy("delete_.*", ["mcp:tools:write"])
+
+ logger.info("Created Turkish legal database policy engine")
+ return engine
+
+
+def create_default_policies() -> PolicyEngine:
+ """Create a default policy set for MCP servers (backwards compatibility)"""
+ return create_turkish_legal_policies()
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py
new file mode 100644
index 0000000..ea1ad8f
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py
@@ -0,0 +1,112 @@
+"""
+Persistent storage for OAuth sessions and tokens
+"""
+
+import json
+import os
+import tempfile
+import logging
+from datetime import datetime
+from typing import Dict, Any, Optional
+
+logger = logging.getLogger(__name__)
+
+
+class PersistentStorage:
+ """File-based persistent storage for OAuth data"""
+
+ def __init__(self, storage_dir: str = None):
+ if storage_dir is None:
+ # Use system temp directory or environment variable
+ storage_dir = os.environ.get('TEMP', tempfile.gettempdir())
+
+ self.storage_dir = os.path.join(storage_dir, 'mcp_oauth_storage')
+ os.makedirs(self.storage_dir, exist_ok=True)
+
+ self.sessions_file = os.path.join(self.storage_dir, 'oauth_sessions.json')
+ self.tokens_file = os.path.join(self.storage_dir, 'oauth_tokens.json')
+
+ logger.info(f"Persistent OAuth storage initialized at: {self.storage_dir}")
+
+ def _load_json(self, filepath: str) -> Dict:
+ """Load JSON data from file"""
+ try:
+ if os.path.exists(filepath):
+ with open(filepath, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except Exception as e:
+ logger.error(f"Error loading {filepath}: {e}")
+ return {}
+
+ def _save_json(self, filepath: str, data: Dict):
+ """Save JSON data to file"""
+ try:
+ with open(filepath, 'w', encoding='utf-8') as f:
+ json.dump(data, f, indent=2, default=str)
+ except Exception as e:
+ logger.error(f"Error saving {filepath}: {e}")
+
+ def get_sessions(self) -> Dict[str, Dict[str, Any]]:
+ """Get all OAuth sessions"""
+ data = self._load_json(self.sessions_file)
+ # Clean expired sessions
+ now = datetime.utcnow().timestamp()
+ valid_sessions = {k: v for k, v in data.items()
+ if v.get('expires_at', 0) > now}
+ if len(valid_sessions) != len(data):
+ self._save_json(self.sessions_file, valid_sessions)
+ return valid_sessions
+
+ def set_session(self, session_id: str, data: Dict[str, Any]):
+ """Set OAuth session data"""
+ sessions = self.get_sessions()
+ sessions[session_id] = data
+ self._save_json(self.sessions_file, sessions)
+
+ def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
+ """Get specific OAuth session data"""
+ sessions = self.get_sessions()
+ return sessions.get(session_id)
+
+ def delete_session(self, session_id: str):
+ """Delete OAuth session"""
+ sessions = self.get_sessions()
+ if session_id in sessions:
+ del sessions[session_id]
+ self._save_json(self.sessions_file, sessions)
+
+ def get_tokens(self) -> Dict[str, Dict[str, Any]]:
+ """Get all OAuth tokens"""
+ data = self._load_json(self.tokens_file)
+ # Clean expired tokens
+ now = datetime.utcnow().timestamp()
+ valid_tokens = {k: v for k, v in data.items()
+ if v.get('expires_at', 0) > now}
+ if len(valid_tokens) != len(data):
+ self._save_json(self.tokens_file, valid_tokens)
+ return valid_tokens
+
+ def set_token(self, token_id: str, token_data: Dict[str, Any]):
+ """Set OAuth token data"""
+ tokens = self.get_tokens()
+ tokens[token_id] = token_data
+ self._save_json(self.tokens_file, tokens)
+
+ def get_token(self, token_id: str) -> Optional[Dict[str, Any]]:
+ """Get specific OAuth token data"""
+ tokens = self.get_tokens()
+ return tokens.get(token_id)
+
+ def delete_token(self, token_id: str):
+ """Delete OAuth token"""
+ tokens = self.get_tokens()
+ if token_id in tokens:
+ del tokens[token_id]
+ self._save_json(self.tokens_file, tokens)
+
+ def cleanup_expired_sessions(self):
+ """Clean up expired sessions and tokens"""
+ # This is handled automatically in get_sessions() and get_tokens()
+ sessions = self.get_sessions()
+ tokens = self.get_tokens()
+ logger.debug(f"Cleanup: {len(sessions)} active sessions, {len(tokens)} active tokens")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py
new file mode 100644
index 0000000..a5760f6
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py
@@ -0,0 +1,193 @@
+"""
+Factory for creating FastMCP app with MCP Auth Toolkit integration
+"""
+
+import logging
+import os
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+try:
+ from fastmcp import FastMCP
+ FASTMCP_AVAILABLE = True
+except ImportError:
+ FASTMCP_AVAILABLE = False
+ FastMCP = None
+
+from mcp_auth import (
+ OAuthProvider,
+ PolicyEngine,
+ FastMCPAuthWrapper,
+ create_default_policies
+)
+from mcp_auth.clerk_config import create_mcp_server_config
+
+
+def create_auth_enabled_app(app_name: str = "Yargı MCP Server") -> FastMCP:
+ """Create FastMCP app with authentication enabled"""
+
+ if not FASTMCP_AVAILABLE:
+ raise ImportError("FastMCP is required for authenticated MCP server")
+
+ logger.info("Creating FastMCP app with MCP Auth Toolkit integration")
+
+ # Create base FastMCP app
+ app = FastMCP(app_name)
+
+ # Check if authentication is enabled
+ auth_enabled = os.getenv("ENABLE_AUTH", "true").lower() == "true"
+
+ if not auth_enabled:
+ logger.info("Authentication disabled, returning basic FastMCP app")
+ return app
+
+ try:
+ # Get configuration
+ logger.info("Getting MCP server configuration...")
+ config = create_mcp_server_config()
+ logger.info("Configuration loaded successfully")
+
+ # Create OAuth provider with Clerk config
+ logger.info("Creating OAuth provider...")
+ oauth_provider = OAuthProvider(
+ config=config["oauth_config"],
+ jwt_secret=config["jwt_secret"]
+ )
+ logger.info("OAuth provider created successfully")
+
+ # Create policy engine for Turkish legal database
+ policy_engine = create_default_policies()
+
+ # Store auth components for later wrapping (after tools are defined)
+ app._oauth_provider = oauth_provider
+ app._policy_engine = policy_engine
+ app._auth_config = config
+
+ # Add OAuth endpoints immediately
+ @app.tool(
+ description="Initiate OAuth 2.1 authorization flow with PKCE",
+ annotations={"readOnlyHint": True, "idempotentHint": False}
+ )
+ async def oauth_authorize(redirect_uri: str, scopes: str = None):
+ """OAuth authorization endpoint"""
+ scope_list = scopes.split(" ") if scopes else ["mcp:tools:read", "mcp:tools:write"]
+ auth_url, pkce = oauth_provider.generate_authorization_url(
+ redirect_uri=redirect_uri, scopes=scope_list
+ )
+ logger.info(f"Generated authorization URL for redirect_uri: {redirect_uri}")
+ return {
+ "authorization_url": auth_url,
+ "code_verifier": pkce.verifier,
+ "code_challenge": pkce.challenge,
+ "instructions": "Use the authorization_url to complete OAuth flow, then exchange the returned code using oauth_token tool"
+ }
+
+ @app.tool(
+ description="Exchange OAuth authorization code for access token",
+ annotations={"readOnlyHint": False, "idempotentHint": False}
+ )
+ async def oauth_token(code: str, state: str, redirect_uri: str):
+ """OAuth token exchange endpoint"""
+ try:
+ result = await oauth_provider.exchange_code_for_token(
+ code=code, state=state, redirect_uri=redirect_uri
+ )
+ logger.info("Successfully exchanged authorization code for token")
+ return result
+ except Exception as e:
+ logger.error(f"Token exchange failed: {e}")
+ raise
+
+ @app.tool(
+ description="Validate and introspect OAuth access token",
+ annotations={"readOnlyHint": True, "idempotentHint": True}
+ )
+ async def oauth_introspect(token: str):
+ """Token introspection endpoint"""
+ result = oauth_provider.introspect_token(token)
+ logger.debug(f"Token introspection: active={result.get('active', False)}")
+ return result
+
+ @app.tool(
+ description="Revoke OAuth access token",
+ annotations={"readOnlyHint": False, "idempotentHint": False}
+ )
+ async def oauth_revoke(token: str):
+ """Token revocation endpoint"""
+ success = oauth_provider.revoke_token(token)
+ logger.info(f"Token revocation: success={success}")
+ return {"revoked": success}
+
+ logger.info("Successfully created authenticated FastMCP app")
+
+ except Exception as e:
+ logger.error(f"Failed to create authenticated app: {e}")
+ logger.info("Falling back to non-authenticated FastMCP app")
+ # Return basic app if auth setup fails
+ return app
+
+ return app
+
+
+def create_app() -> FastMCP:
+ """Create FastMCP app (backwards compatible with mcp_factory.py)"""
+ return create_auth_enabled_app()
+
+
+def get_auth_wrapper(app: FastMCP) -> Optional[FastMCPAuthWrapper]:
+ """Get auth wrapper from app if available"""
+ return getattr(app, '_auth_wrapper', None)
+
+
+def get_oauth_provider(app: FastMCP) -> Optional[OAuthProvider]:
+ """Get OAuth provider from app if available"""
+ return getattr(app, '_oauth_provider', None)
+
+
+def get_policy_engine(app: FastMCP) -> Optional[PolicyEngine]:
+ """Get policy engine from app if available"""
+ return getattr(app, '_policy_engine', None)
+
+
+def is_auth_enabled(app: FastMCP) -> bool:
+ """Check if authentication is enabled for the app"""
+ return hasattr(app, '_oauth_provider') or hasattr(app, '_auth_wrapper')
+
+
+def enable_tool_authentication(app: FastMCP):
+ """Enable authentication on all existing tools (call after tools are defined)"""
+ if not is_auth_enabled(app):
+ logger.debug("Authentication not enabled, skipping tool authentication")
+ return
+
+ oauth_provider = get_oauth_provider(app)
+ policy_engine = get_policy_engine(app)
+
+ if not oauth_provider or not policy_engine:
+ logger.warning("OAuth provider or policy engine not available")
+ return
+
+ try:
+ # Create auth wrapper and wrap tools
+ auth_wrapper = FastMCPAuthWrapper(
+ mcp_server=app,
+ oauth_provider=oauth_provider,
+ policy_engine=policy_engine
+ )
+
+ # Store wrapper for reference
+ app._auth_wrapper = auth_wrapper
+
+ logger.info("Tool authentication enabled successfully")
+
+ except Exception as e:
+ logger.error(f"Failed to enable tool authentication: {e}")
+
+
+def cleanup_auth_sessions(app: FastMCP):
+ """Clean up expired auth sessions and tokens"""
+ oauth_provider = get_oauth_provider(app)
+ if oauth_provider:
+ oauth_provider.cleanup_expired_sessions()
+ logger.debug("Cleaned up expired OAuth sessions")
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py
new file mode 100644
index 0000000..442b540
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py
@@ -0,0 +1,383 @@
+"""
+HTTP adapter for MCP Auth Toolkit OAuth endpoints
+Exposes MCP OAuth tools as HTTP endpoints for Claude.ai integration
+"""
+
+import os
+import logging
+import secrets
+import time
+from typing import Optional
+from urllib.parse import urlencode, quote
+from datetime import datetime, timedelta
+
+from fastapi import APIRouter, Request, Query, HTTPException
+from fastapi.responses import RedirectResponse, JSONResponse
+
+# Try to import Clerk SDK
+try:
+ from clerk_backend_api import Clerk
+ CLERK_AVAILABLE = True
+except ImportError as e:
+ CLERK_AVAILABLE = False
+ Clerk = None
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+# OAuth configuration
+BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
+
+
+@router.get("/.well-known/oauth-authorization-server")
+async def get_oauth_metadata():
+ """OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
+ return JSONResponse({
+ "issuer": BASE_URL,
+ "authorization_endpoint": f"{BASE_URL}/authorize",
+ "token_endpoint": f"{BASE_URL}/token",
+ "registration_endpoint": f"{BASE_URL}/register",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "code_challenge_methods_supported": ["S256"],
+ "token_endpoint_auth_methods_supported": ["none"],
+ "scopes_supported": ["mcp:tools:read", "mcp:tools:write", "openid", "profile", "email"],
+ "service_documentation": f"{BASE_URL}/mcp/"
+ })
+
+
+@router.get("/.well-known/oauth-protected-resource")
+async def get_protected_resource_metadata():
+ """OAuth Protected Resource Metadata (RFC 9728)"""
+ return JSONResponse({
+ "resource": BASE_URL,
+ "authorization_servers": [BASE_URL],
+ "bearer_methods_supported": ["header"],
+ "scopes_supported": ["mcp:tools:read", "mcp:tools:write"],
+ "resource_documentation": f"{BASE_URL}/docs"
+ })
+
+
+@router.get("/authorize")
+async def authorize_endpoint(
+ response_type: str = Query(...),
+ client_id: str = Query(...),
+ redirect_uri: str = Query(...),
+ code_challenge: str = Query(...),
+ code_challenge_method: str = Query("S256"),
+ state: Optional[str] = Query(None),
+ scope: Optional[str] = Query(None)
+):
+ """OAuth 2.1 Authorization Endpoint - Uses Clerk SDK for custom domains"""
+
+ logger.info(f"OAuth authorize request - client_id: {client_id}, redirect_uri: {redirect_uri}")
+
+ if not CLERK_AVAILABLE:
+ logger.error("Clerk SDK not available")
+ raise HTTPException(status_code=500, detail="Clerk SDK not available")
+
+ # Store OAuth session for later validation
+ try:
+ from mcp_server_main import app as mcp_app
+ from mcp_auth_factory import get_oauth_provider
+
+ oauth_provider = get_oauth_provider(mcp_app)
+ if not oauth_provider:
+ raise HTTPException(status_code=500, detail="OAuth provider not configured")
+
+ # Generate session and store PKCE
+ session_id = secrets.token_urlsafe(32)
+ if state is None:
+ state = secrets.token_urlsafe(16)
+
+ # Create PKCE challenge
+ from mcp_auth.oauth import PKCEChallenge
+ pkce = PKCEChallenge()
+
+ # Store session data
+ session_data = {
+ "pkce_verifier": pkce.verifier,
+ "pkce_challenge": code_challenge, # Store the client's challenge
+ "state": state,
+ "redirect_uri": redirect_uri,
+ "client_id": client_id,
+ "scopes": scope.split(" ") if scope else ["mcp:tools:read", "mcp:tools:write"],
+ "created_at": time.time(),
+ "expires_at": (datetime.utcnow() + timedelta(minutes=10)).timestamp(),
+ }
+ oauth_provider.storage.set_session(session_id, session_data)
+
+ # For Clerk with custom domains, we need to use their hosted sign-in page
+ # We'll pass our callback URL and session info in the state
+ callback_url = f"{BASE_URL}/auth/callback"
+
+ # Encode session info in state for retrieval after Clerk auth
+ combined_state = f"{state}:{session_id}"
+
+ # Use Clerk's sign-in URL with proper parameters
+ clerk_domain = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com")
+ sign_in_params = {
+ "redirect_url": f"{callback_url}?state={quote(combined_state)}",
+ }
+
+ sign_in_url = f"https://{clerk_domain}/sign-in?{urlencode(sign_in_params)}"
+
+ logger.info(f"Redirecting to Clerk sign-in: {sign_in_url}")
+
+ return RedirectResponse(url=sign_in_url)
+
+ except Exception as e:
+ logger.exception(f"Authorization failed: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/auth/callback")
+async def oauth_callback(
+ request: Request,
+ state: Optional[str] = Query(None),
+ clerk_token: Optional[str] = Query(None)
+):
+ """Handle OAuth callback from Clerk - supports both JWT token and cookie auth"""
+
+ logger.info(f"OAuth callback received - state: {state}")
+ logger.info(f"Query params: {dict(request.query_params)}")
+ logger.info(f"Cookies: {dict(request.cookies)}")
+ logger.info(f"Clerk JWT token provided: {bool(clerk_token)}")
+
+ # Support both JWT token (for cross-domain) and cookie auth (for subdomain)
+
+ try:
+ if not state:
+ logger.error("No state parameter provided")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "Missing state parameter"}
+ )
+
+ # Parse state to get original state and session ID
+ try:
+ if ":" in state:
+ original_state, session_id = state.rsplit(":", 1)
+ else:
+ original_state = state
+ session_id = state # Fallback
+ except ValueError:
+ logger.error(f"Invalid state format: {state}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "Invalid state format"}
+ )
+
+ # Get OAuth provider
+ from mcp_server_main import app as mcp_app
+ from mcp_auth_factory import get_oauth_provider
+
+ oauth_provider = get_oauth_provider(mcp_app)
+ if not oauth_provider:
+ raise HTTPException(status_code=500, detail="OAuth provider not configured")
+
+ # Get stored session
+ oauth_session = oauth_provider.storage.get_session(session_id)
+
+ if not oauth_session:
+ logger.error(f"OAuth session not found for ID: {session_id}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "OAuth session expired or not found"}
+ )
+
+ # Check if we have a JWT token (for cross-domain auth)
+ user_authenticated = False
+ auth_method = "none"
+
+ if clerk_token:
+ logger.info("Attempting JWT token validation")
+ try:
+ # Validate JWT token with Clerk
+ from clerk_backend_api import Clerk
+ clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
+
+ # Extract session_id from JWT token and verify with Clerk
+ import jwt
+ decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
+ session_id = decoded_token.get("sid") or decoded_token.get("session_id")
+
+ if session_id:
+ # Verify with Clerk using session_id
+ session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
+ user_id = session.user_id if session else None
+ else:
+ user_id = None
+
+ if user_id:
+ logger.info(f"JWT token validation successful - user_id: {user_id}")
+ user_authenticated = True
+ auth_method = "jwt_token"
+ # Store user info in session for token exchange
+ oauth_session["user_id"] = user_id
+ oauth_session["auth_method"] = "jwt_token"
+ else:
+ logger.error("JWT token validation failed - no user_id in claims")
+ except Exception as e:
+ logger.error(f"JWT token validation failed: {str(e)}")
+ # Fall through to cookie validation
+
+ # If no JWT token or validation failed, check cookies
+ if not user_authenticated:
+ logger.info("Checking for Clerk session cookies")
+ # Check for Clerk session cookies (for subdomain auth)
+ clerk_session_cookie = request.cookies.get("__session")
+ if clerk_session_cookie:
+ logger.info("Found Clerk session cookie, assuming authenticated")
+ user_authenticated = True
+ auth_method = "cookie"
+ oauth_session["auth_method"] = "cookie"
+ else:
+ logger.info("No Clerk session cookie found")
+
+ # For custom domains, we'll also trust that Clerk redirected here
+ if not user_authenticated:
+ logger.info("Trusting Clerk redirect for custom domain flow")
+ user_authenticated = True
+ auth_method = "trusted_redirect"
+ oauth_session["auth_method"] = "trusted_redirect"
+
+ logger.info(f"User authenticated: {user_authenticated}, method: {auth_method}")
+
+ # Generate simple authorization code for custom domain flow
+ auth_code = f"clerk_custom_{session_id}_{int(time.time())}"
+
+ # Store the code mapping for token exchange
+ code_data = {
+ "session_id": session_id,
+ "clerk_authenticated": user_authenticated,
+ "auth_method": auth_method,
+ "custom_domain_flow": True,
+ "created_at": time.time(),
+ "expires_at": (datetime.utcnow() + timedelta(minutes=5)).timestamp(),
+ }
+ if "user_id" in oauth_session:
+ code_data["user_id"] = oauth_session["user_id"]
+
+ oauth_provider.storage.set_session(f"code_{auth_code}", code_data)
+
+ # Build redirect URL back to Claude
+ redirect_params = {
+ "code": auth_code,
+ "state": original_state
+ }
+
+ redirect_url = f"{oauth_session['redirect_uri']}?{urlencode(redirect_params)}"
+ logger.info(f"Redirecting back to Claude: {redirect_url}")
+
+ return RedirectResponse(url=redirect_url)
+
+ except Exception as e:
+ logger.exception(f"Callback processing failed: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": "server_error", "error_description": str(e)}
+ )
+
+
+@router.post("/register")
+async def register_client(request: Request):
+ """Dynamic Client Registration (RFC 7591)"""
+
+ data = await request.json()
+ logger.info(f"Client registration request: {data}")
+
+ # Simple dynamic registration - accept any client
+ client_id = f"mcp-client-{os.urandom(8).hex()}"
+
+ return JSONResponse({
+ "client_id": client_id,
+ "client_secret": None, # Public client
+ "redirect_uris": data.get("redirect_uris", []),
+ "grant_types": ["authorization_code", "refresh_token"],
+ "response_types": ["code"],
+ "client_name": data.get("client_name", "MCP Client"),
+ "token_endpoint_auth_method": "none",
+ "client_id_issued_at": int(datetime.now().timestamp())
+ })
+
+
+@router.post("/token")
+async def token_endpoint(request: Request):
+ """OAuth 2.1 Token Endpoint"""
+
+ # Parse form data
+ form_data = await request.form()
+ grant_type = form_data.get("grant_type")
+ code = form_data.get("code")
+ redirect_uri = form_data.get("redirect_uri")
+ client_id = form_data.get("client_id")
+ code_verifier = form_data.get("code_verifier")
+
+ logger.info(f"Token exchange - grant_type: {grant_type}, code: {code[:20] if code else 'None'}...")
+
+ if grant_type != "authorization_code":
+ return JSONResponse(
+ status_code=400,
+ content={"error": "unsupported_grant_type"}
+ )
+
+ try:
+ # OAuth token exchange - validate code and return Clerk JWT
+ # This supports proper OAuth flow while using Clerk JWT tokens
+
+ if not code or not redirect_uri:
+ logger.error("Missing required parameters: code or redirect_uri")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
+ )
+
+ # Validate OAuth code with Clerk
+ if CLERK_AVAILABLE:
+ try:
+ clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
+
+ # In a real implementation, you'd validate the code with Clerk
+ # For now, we'll assume the code is valid if it looks like a Clerk code
+ if len(code) > 10: # Basic validation
+ # Create a mock session with the code
+ # In practice, this would be validated with Clerk's OAuth flow
+
+ # Return Clerk JWT token format
+ # This should be the actual Clerk JWT token from the OAuth flow
+ return JSONResponse({
+ "access_token": f"mock_clerk_jwt_{code}",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "yargi.read yargi.search"
+ })
+ else:
+ logger.error(f"Invalid code format: {code}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
+ )
+
+ except Exception as e:
+ logger.error(f"Clerk validation failed: {e}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Authorization code validation failed"}
+ )
+ else:
+ logger.warning("Clerk SDK not available, using mock response")
+ return JSONResponse({
+ "access_token": "mock_jwt_token_for_development",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "yargi.read yargi.search"
+ })
+
+ except Exception as e:
+ logger.exception(f"Token exchange failed: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": "server_error", "error_description": str(e)}
+ )
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py
new file mode 100644
index 0000000..4311ef9
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py
@@ -0,0 +1,522 @@
+"""
+Simplified MCP OAuth HTTP adapter - only Clerk JWT based authentication
+Uses Redis for authorization code storage to support multi-machine deployment
+"""
+
+import os
+import logging
+from typing import Optional
+from urllib.parse import urlencode, quote
+
+from fastapi import APIRouter, Request, Query, HTTPException
+from fastapi.responses import RedirectResponse, JSONResponse
+
+# Import Redis session store
+from redis_session_store import get_redis_store
+
+# Try to import Clerk SDK
+try:
+ from clerk_backend_api import Clerk
+ CLERK_AVAILABLE = True
+except ImportError:
+ CLERK_AVAILABLE = False
+ Clerk = None
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+# OAuth configuration
+BASE_URL = os.getenv("BASE_URL", "https://api.yargimcp.com")
+CLERK_DOMAIN = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com")
+
+# Initialize Redis store
+redis_store = None
+
+def get_redis_session_store():
+ """Get Redis store instance with lazy initialization."""
+ global redis_store
+ if redis_store is None:
+ try:
+ import concurrent.futures
+ import functools
+
+ # Use thread pool with timeout to prevent hanging
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(get_redis_store)
+ try:
+ # 5 second timeout for Redis initialization
+ redis_store = future.result(timeout=5.0)
+ if redis_store:
+ logger.info("Redis session store initialized for OAuth handler")
+ else:
+ logger.warning("Redis store initialization returned None")
+ except concurrent.futures.TimeoutError:
+ logger.error("Redis initialization timed out after 5 seconds")
+ redis_store = None
+ future.cancel() # Try to cancel the hanging operation
+
+ except Exception as e:
+ logger.error(f"Failed to initialize Redis store: {e}")
+ redis_store = None
+
+ if redis_store is None:
+ # Fall back to in-memory storage with warning
+ logger.warning("Falling back to in-memory storage - multi-machine deployment will not work")
+
+ return redis_store
+
+@router.get("/.well-known/oauth-authorization-server")
+async def get_oauth_metadata():
+ """OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
+ return JSONResponse({
+ "issuer": BASE_URL,
+ "authorization_endpoint": "https://yargimcp.com/mcp-callback",
+ "token_endpoint": f"{BASE_URL}/token",
+ "registration_endpoint": f"{BASE_URL}/register",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code"],
+ "code_challenge_methods_supported": ["S256"],
+ "token_endpoint_auth_methods_supported": ["none"],
+ "scopes_supported": ["read", "search", "openid", "profile", "email"],
+ "service_documentation": f"{BASE_URL}/mcp/"
+ })
+
+@router.get("/auth/login")
+async def oauth_authorize(
+ request: Request,
+ client_id: str = Query(...),
+ redirect_uri: str = Query(...),
+ response_type: str = Query("code"),
+ scope: Optional[str] = Query("read search"),
+ state: Optional[str] = Query(None),
+ code_challenge: Optional[str] = Query(None),
+ code_challenge_method: Optional[str] = Query(None)
+):
+ """OAuth 2.1 Authorization Endpoint - redirects to Clerk"""
+
+ logger.info(f"OAuth authorize request - client_id: {client_id}")
+ logger.info(f"Redirect URI: {redirect_uri}")
+ logger.info(f"State: {state}")
+ logger.info(f"PKCE Challenge: {bool(code_challenge)}")
+
+ try:
+ # Build callback URL with all necessary parameters
+ callback_url = f"{BASE_URL}/auth/callback"
+ callback_params = {
+ "client_id": client_id,
+ "redirect_uri": redirect_uri,
+ "state": state or "",
+ "scope": scope or "read search"
+ }
+
+ # Add PKCE parameters if present
+ if code_challenge:
+ callback_params["code_challenge"] = code_challenge
+ callback_params["code_challenge_method"] = code_challenge_method or "S256"
+
+ # Encode callback URL as redirect_url for Clerk
+ callback_with_params = f"{callback_url}?{urlencode(callback_params)}"
+
+ # Build Clerk sign-in URL - use yargimcp.com frontend for JWT token generation
+ clerk_params = {
+ "redirect_url": callback_with_params
+ }
+
+ # Use frontend sign-in page that handles JWT token generation
+ clerk_signin_url = f"https://yargimcp.com/sign-in?{urlencode(clerk_params)}"
+
+ logger.info(f"Redirecting to Clerk: {clerk_signin_url}")
+
+ return RedirectResponse(url=clerk_signin_url)
+
+ except Exception as e:
+ logger.exception(f"Authorization failed: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@router.get("/auth/callback")
+async def oauth_callback(
+ request: Request,
+ client_id: str = Query(...),
+ redirect_uri: str = Query(...),
+ state: Optional[str] = Query(None),
+ scope: Optional[str] = Query("read search"),
+ code_challenge: Optional[str] = Query(None),
+ code_challenge_method: Optional[str] = Query(None),
+ clerk_token: Optional[str] = Query(None)
+):
+ """OAuth callback from Clerk - generates authorization code"""
+
+ logger.info(f"OAuth callback - client_id: {client_id}")
+ logger.info(f"Clerk token provided: {bool(clerk_token)}")
+
+ try:
+ # Validate user with Clerk and generate real JWT token
+ user_authenticated = False
+ user_id = None
+ session_id = None
+ real_jwt_token = None
+
+ if clerk_token and CLERK_AVAILABLE:
+ try:
+ # Extract user info from JWT token (no Clerk session verification needed)
+ import jwt
+ decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
+ user_id = decoded_token.get("user_id") or decoded_token.get("sub")
+ user_email = decoded_token.get("email")
+ token_scopes = decoded_token.get("scopes", ["read", "search"])
+
+ logger.info(f"JWT token claims - user_id: {user_id}, email: {user_email}, scopes: {token_scopes}")
+
+ if user_id and user_email:
+ # JWT token is already signed by Clerk and contains valid user info
+ user_authenticated = True
+ logger.info(f"User authenticated via JWT token - user_id: {user_id}")
+
+ # Use the JWT token directly as the real token (it's already from Clerk template)
+ real_jwt_token = clerk_token
+ logger.info("Using Clerk JWT token directly (already real token)")
+
+ else:
+ logger.error(f"Missing required fields in JWT token - user_id: {bool(user_id)}, email: {bool(user_email)}")
+
+ except Exception as e:
+ logger.error(f"JWT validation failed: {e}")
+
+ # Fallback to cookie validation
+ if not user_authenticated:
+ clerk_session = request.cookies.get("__session")
+ if clerk_session:
+ user_authenticated = True
+ logger.info("User authenticated via cookie")
+
+ # Try to get session from cookie and generate JWT
+ if CLERK_AVAILABLE:
+ try:
+ clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
+ # Note: sessions.verify_session is deprecated, but we'll try
+ # In practice, you'd need to extract session_id from cookie
+ logger.info("Cookie authentication - JWT generation not implemented yet")
+ except Exception as e:
+ logger.warning(f"Failed to generate JWT from cookie: {e}")
+
+ # Only generate authorization code if we have a real JWT token
+ if user_authenticated and real_jwt_token:
+ # Generate authorization code
+ auth_code = f"clerk_auth_{os.urandom(16).hex()}"
+
+ # Prepare code data
+ import time
+ code_data = {
+ "user_id": user_id,
+ "session_id": session_id,
+ "real_jwt_token": real_jwt_token,
+ "user_authenticated": user_authenticated,
+ "client_id": client_id,
+ "redirect_uri": redirect_uri,
+ "scope": scope or "read search"
+ }
+
+ # Try to store in Redis, fall back to in-memory if Redis unavailable
+ store = get_redis_session_store()
+ if store:
+ # Store in Redis with automatic expiration
+ success = store.set_oauth_code(auth_code, code_data)
+ if success:
+ logger.info(f"Stored authorization code {auth_code[:10]}... in Redis with real JWT token")
+ else:
+ logger.error(f"Failed to store authorization code in Redis, falling back to in-memory")
+ # Fall back to in-memory storage
+ if not hasattr(oauth_callback, '_code_storage'):
+ oauth_callback._code_storage = {}
+ oauth_callback._code_storage[auth_code] = code_data
+ else:
+ # Fall back to in-memory storage
+ logger.warning("Redis not available, using in-memory storage")
+ if not hasattr(oauth_callback, '_code_storage'):
+ oauth_callback._code_storage = {}
+ oauth_callback._code_storage[auth_code] = code_data
+ logger.info(f"Stored authorization code in memory (fallback)")
+
+ # Redirect back to client with authorization code
+ redirect_params = {
+ "code": auth_code,
+ "state": state or ""
+ }
+
+ final_redirect_url = f"{redirect_uri}?{urlencode(redirect_params)}"
+ logger.info(f"Redirecting back to client: {final_redirect_url}")
+
+ return RedirectResponse(url=final_redirect_url)
+ else:
+ # No JWT token yet - redirect back to sign-in page to wait for authentication
+ logger.info("No JWT token provided - redirecting back to sign-in to complete authentication")
+
+ # Keep the same redirect URL so the flow continues
+ sign_in_params = {
+ "redirect_url": f"{request.url._url}" # Current callback URL with all params
+ }
+
+ sign_in_url = f"https://yargimcp.com/sign-in?{urlencode(sign_in_params)}"
+ logger.info(f"Redirecting back to sign-in: {sign_in_url}")
+
+ return RedirectResponse(url=sign_in_url)
+
+ except Exception as e:
+ logger.exception(f"Callback processing failed: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": "server_error", "error_description": str(e)}
+ )
+
+@router.post("/auth/register")
+async def register_client(request: Request):
+ """Dynamic Client Registration (RFC 7591)"""
+
+ data = await request.json()
+ logger.info(f"Client registration request: {data}")
+
+ # Simple dynamic registration - accept any client
+ client_id = f"mcp-client-{os.urandom(8).hex()}"
+
+ return JSONResponse({
+ "client_id": client_id,
+ "client_secret": None, # Public client
+ "redirect_uris": data.get("redirect_uris", []),
+ "grant_types": ["authorization_code"],
+ "response_types": ["code"],
+ "client_name": data.get("client_name", "MCP Client"),
+ "token_endpoint_auth_method": "none"
+ })
+
+@router.post("/auth/callback")
+async def oauth_callback_post(request: Request):
+ """OAuth callback POST endpoint for token exchange"""
+
+ # Parse form data (standard OAuth token exchange format)
+ form_data = await request.form()
+ grant_type = form_data.get("grant_type")
+ code = form_data.get("code")
+ redirect_uri = form_data.get("redirect_uri")
+ client_id = form_data.get("client_id")
+ code_verifier = form_data.get("code_verifier")
+
+ logger.info(f"OAuth callback POST - grant_type: {grant_type}")
+ logger.info(f"Code: {code[:20] if code else 'None'}...")
+ logger.info(f"Client ID: {client_id}")
+ logger.info(f"PKCE verifier: {bool(code_verifier)}")
+
+ if grant_type != "authorization_code":
+ return JSONResponse(
+ status_code=400,
+ content={"error": "unsupported_grant_type"}
+ )
+
+ if not code or not redirect_uri:
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
+ )
+
+ try:
+ # Validate authorization code
+ if not code.startswith("clerk_auth_"):
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
+ )
+
+ # Retrieve stored JWT token using authorization code from Redis or in-memory fallback
+ stored_code_data = None
+
+ # Try to get from Redis first, then fall back to in-memory
+ store = get_redis_session_store()
+ if store:
+ stored_code_data = store.get_oauth_code(code, delete_after_use=True)
+ if stored_code_data:
+ logger.info(f"Retrieved authorization code {code[:10]}... from Redis")
+ else:
+ logger.warning(f"Authorization code {code[:10]}... not found in Redis")
+
+ # Fall back to in-memory storage if Redis unavailable or code not found
+ if not stored_code_data and hasattr(oauth_callback, '_code_storage'):
+ stored_code_data = oauth_callback._code_storage.get(code)
+ if stored_code_data:
+ # Clean up in-memory storage
+ oauth_callback._code_storage.pop(code, None)
+ logger.info(f"Retrieved authorization code {code[:10]}... from in-memory storage")
+
+ if not stored_code_data:
+ logger.error(f"No stored data found for authorization code: {code}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"}
+ )
+
+ # Note: Redis TTL handles expiration automatically, but check for manual expiration for in-memory fallback
+ import time
+ expires_at = stored_code_data.get("expires_at", 0)
+ if expires_at and time.time() > expires_at:
+ logger.error(f"Authorization code expired: {code}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Authorization code expired"}
+ )
+
+ # Get the real JWT token
+ real_jwt_token = stored_code_data.get("real_jwt_token")
+
+ if real_jwt_token:
+ logger.info("Returning real Clerk JWT token")
+ # Note: Code already deleted from Redis, clean up in-memory fallback if used
+ if hasattr(oauth_callback, '_code_storage'):
+ oauth_callback._code_storage.pop(code, None)
+
+ return JSONResponse({
+ "access_token": real_jwt_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read search"
+ })
+ else:
+ logger.warning("No real JWT token found, generating mock token")
+ # Fallback to mock token for testing
+ mock_token = f"mock_clerk_jwt_{code}"
+ return JSONResponse({
+ "access_token": mock_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read search"
+ })
+
+ except Exception as e:
+ logger.exception(f"OAuth callback POST failed: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": "server_error", "error_description": str(e)}
+ )
+
+@router.post("/register")
+async def register_client(request: Request):
+ """Dynamic Client Registration (RFC 7591)"""
+
+ data = await request.json()
+ logger.info(f"Client registration request: {data}")
+
+ # Simple dynamic registration - accept any client
+ client_id = f"mcp-client-{os.urandom(8).hex()}"
+
+ return JSONResponse({
+ "client_id": client_id,
+ "client_secret": None, # Public client
+ "redirect_uris": data.get("redirect_uris", []),
+ "grant_types": ["authorization_code"],
+ "response_types": ["code"],
+ "client_name": data.get("client_name", "MCP Client"),
+ "token_endpoint_auth_method": "none"
+ })
+
+@router.post("/token")
+async def token_endpoint(request: Request):
+ """OAuth 2.1 Token Endpoint - exchanges code for Clerk JWT"""
+
+ # Parse form data
+ form_data = await request.form()
+ grant_type = form_data.get("grant_type")
+ code = form_data.get("code")
+ redirect_uri = form_data.get("redirect_uri")
+ client_id = form_data.get("client_id")
+ code_verifier = form_data.get("code_verifier")
+
+ logger.info(f"Token exchange - grant_type: {grant_type}")
+ logger.info(f"Code: {code[:20] if code else 'None'}...")
+
+ if grant_type != "authorization_code":
+ return JSONResponse(
+ status_code=400,
+ content={"error": "unsupported_grant_type"}
+ )
+
+ if not code or not redirect_uri:
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
+ )
+
+ try:
+ # Validate authorization code
+ if not code.startswith("clerk_auth_"):
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
+ )
+
+ # Retrieve stored JWT token using authorization code from Redis or in-memory fallback
+ stored_code_data = None
+
+ # Try to get from Redis first, then fall back to in-memory
+ store = get_redis_session_store()
+ if store:
+ stored_code_data = store.get_oauth_code(code, delete_after_use=True)
+ if stored_code_data:
+ logger.info(f"Retrieved authorization code {code[:10]}... from Redis (/token endpoint)")
+ else:
+ logger.warning(f"Authorization code {code[:10]}... not found in Redis (/token endpoint)")
+
+ # Fall back to in-memory storage if Redis unavailable or code not found
+ if not stored_code_data and hasattr(oauth_callback, '_code_storage'):
+ stored_code_data = oauth_callback._code_storage.get(code)
+ if stored_code_data:
+ # Clean up in-memory storage
+ oauth_callback._code_storage.pop(code, None)
+ logger.info(f"Retrieved authorization code {code[:10]}... from in-memory storage (/token endpoint)")
+
+ if not stored_code_data:
+ logger.error(f"No stored data found for authorization code: {code}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"}
+ )
+
+ # Note: Redis TTL handles expiration automatically, but check for manual expiration for in-memory fallback
+ import time
+ expires_at = stored_code_data.get("expires_at", 0)
+ if expires_at and time.time() > expires_at:
+ logger.error(f"Authorization code expired: {code}")
+ return JSONResponse(
+ status_code=400,
+ content={"error": "invalid_grant", "error_description": "Authorization code expired"}
+ )
+
+ # Get the real JWT token
+ real_jwt_token = stored_code_data.get("real_jwt_token")
+
+ if real_jwt_token:
+ logger.info("Returning real Clerk JWT token from /token endpoint")
+ # Note: Code already deleted from Redis, clean up in-memory fallback if used
+ if hasattr(oauth_callback, '_code_storage'):
+ oauth_callback._code_storage.pop(code, None)
+
+ return JSONResponse({
+ "access_token": real_jwt_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read search"
+ })
+ else:
+ logger.warning("No real JWT token found in /token endpoint, generating mock token")
+ # Fallback to mock token for testing
+ mock_token = f"mock_clerk_jwt_{code}"
+ return JSONResponse({
+ "access_token": mock_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": "read search"
+ })
+
+ except Exception as e:
+ logger.exception(f"Token exchange failed: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": "server_error", "error_description": str(e)}
+ )
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py b/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py
new file mode 100644
index 0000000..7c3e512
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py
@@ -0,0 +1,2883 @@
+# mcp_server_main.py
+import asyncio
+import atexit
+import logging
+import os
+import httpx
+import json
+import time
+from collections import defaultdict
+from pydantic import HttpUrl, Field
+from typing import Optional, Dict, List, Literal, Any, Union
+import urllib.parse
+import tiktoken
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.server.dependencies import get_access_token, AccessToken
+from fastmcp import Context
+
+# Use standard exception for tool errors
+class ToolError(Exception):
+ """Tool execution error"""
+ 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)
+
+console_handler = logging.StreamHandler()
+console_handler.setFormatter(log_formatter)
+console_handler.setLevel(logging.INFO)
+root_logger.addHandler(console_handler)
+
+logger = logging.getLogger(__name__)
+# --- Logging Configuration End ---
+
+# --- Token Counting Middleware ---
+class TokenCountingMiddleware(Middleware):
+ """Middleware for counting input/output tokens using tiktoken."""
+
+ def __init__(self, model: str = "cl100k_base"):
+ """Initialize token counting middleware.
+
+ Args:
+ model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
+ """
+ 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:
+ """Count tokens in text using tiktoken."""
+ if not text:
+ return 0
+ try:
+ return len(self.encoder.encode(str(text)))
+ except Exception as e:
+ logger.warning(f"Token counting failed: {e}")
+ return 0
+
+ def extract_text_content(self, data: Any) -> str:
+ """Extract text content from various data types."""
+ if isinstance(data, str):
+ return data
+ elif isinstance(data, dict):
+ # Extract text from common response fields
+ text_parts = []
+ for key, value in data.items():
+ if isinstance(value, str):
+ text_parts.append(value)
+ elif isinstance(value, list):
+ for item in value:
+ if isinstance(item, str):
+ text_parts.append(item)
+ elif isinstance(item, dict) and 'text' in item:
+ text_parts.append(str(item['text']))
+ return ' '.join(text_parts)
+ elif isinstance(data, list):
+ text_parts = []
+ for item in data:
+ text_parts.append(self.extract_text_content(item))
+ return ' '.join(text_parts)
+ else:
+ return str(data)
+
+ def log_token_usage(self, operation: str, input_tokens: int, output_tokens: int,
+ tool_name: str = None, duration_ms: float = None):
+ """Log token usage with structured format."""
+ log_data = {
+ "operation": operation,
+ "tool_name": tool_name,
+ "input_tokens": input_tokens,
+ "output_tokens": output_tokens,
+ "total_tokens": input_tokens + output_tokens,
+ "duration_ms": duration_ms,
+ "timestamp": time.time()
+ }
+
+ # Update statistics
+ key = tool_name if tool_name else operation
+ self.token_stats[key]["input"] += input_tokens
+ self.token_stats[key]["output"] += output_tokens
+ self.token_stats[key]["calls"] += 1
+
+ # Log as JSON for easy parsing
+ self.logger.info(json.dumps(log_data))
+
+ # Also log human-readable format to main logger
+ logger.info(f"Token Usage - {operation}" +
+ (f" ({tool_name})" if tool_name else "") +
+ f": {input_tokens} in + {output_tokens} out = {input_tokens + output_tokens} total")
+
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ """Count tokens for tool calls."""
+ start_time = time.perf_counter()
+
+ # Extract tool name and arguments
+ tool_name = getattr(context.message, 'name', 'unknown_tool')
+ tool_args = getattr(context.message, 'arguments', {})
+
+ # Count input tokens (tool arguments)
+ input_text = self.extract_text_content(tool_args)
+ input_tokens = self.count_tokens(input_text)
+
+ try:
+ # Execute the tool
+ result = await call_next(context)
+
+ # Count output tokens (tool result)
+ output_text = self.extract_text_content(result)
+ output_tokens = self.count_tokens(output_text)
+
+ # Calculate duration
+ duration_ms = (time.perf_counter() - start_time) * 1000
+
+ # Log token usage
+ self.log_token_usage("tool_call", input_tokens, output_tokens,
+ tool_name, duration_ms)
+
+ return result
+
+ except Exception as e:
+ duration_ms = (time.perf_counter() - start_time) * 1000
+ self.log_token_usage("tool_call_error", input_tokens, 0,
+ tool_name, duration_ms)
+ raise
+
+ async def on_read_resource(self, context: MiddlewareContext, call_next):
+ """Count tokens for resource reads."""
+ start_time = time.perf_counter()
+
+ # Extract resource URI
+ resource_uri = getattr(context.message, 'uri', 'unknown_resource')
+
+ try:
+ # Execute the resource read
+ result = await call_next(context)
+
+ # Count output tokens (resource content)
+ output_text = self.extract_text_content(result)
+ output_tokens = self.count_tokens(output_text)
+
+ # Calculate duration
+ duration_ms = (time.perf_counter() - start_time) * 1000
+
+ # Log token usage (no input tokens for resource reads)
+ self.log_token_usage("resource_read", 0, output_tokens,
+ resource_uri, duration_ms)
+
+ return result
+
+ except Exception as e:
+ duration_ms = (time.perf_counter() - start_time) * 1000
+ self.log_token_usage("resource_read_error", 0, 0,
+ resource_uri, duration_ms)
+ raise
+
+ async def on_get_prompt(self, context: MiddlewareContext, call_next):
+ """Count tokens for prompt retrievals."""
+ start_time = time.perf_counter()
+
+ # Extract prompt name
+ prompt_name = getattr(context.message, 'name', 'unknown_prompt')
+
+ try:
+ # Execute the prompt retrieval
+ result = await call_next(context)
+
+ # Count output tokens (prompt content)
+ output_text = self.extract_text_content(result)
+ output_tokens = self.count_tokens(output_text)
+
+ # Calculate duration
+ duration_ms = (time.perf_counter() - start_time) * 1000
+
+ # Log token usage
+ self.log_token_usage("prompt_get", 0, output_tokens,
+ prompt_name, duration_ms)
+
+ return result
+
+ except Exception as e:
+ duration_ms = (time.perf_counter() - start_time) * 1000
+ self.log_token_usage("prompt_get_error", 0, 0,
+ prompt_name, duration_ms)
+ raise
+
+ def get_token_stats(self) -> Dict[str, Any]:
+ """Get current token usage statistics."""
+ return dict(self.token_stats)
+
+ def reset_token_stats(self):
+ """Reset token usage statistics."""
+ self.token_stats.clear()
+
+# --- End Token Counting Middleware ---
+
+# Create FastMCP app directly without authentication wrapper
+from fastmcp import FastMCP
+
+def create_app(auth=None):
+ """Create FastMCP app with standard capabilities and optional auth."""
+ global app
+ if auth:
+ # Set auth on existing app instead of creating new one
+ app.auth = auth
+ app.name = "Yargı MCP Server"
+ logger.info("MCP server created with Bearer authentication enabled")
+ else:
+ # Update placeholder app name only
+ app.name = "Yargı MCP Server"
+ logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)")
+
+ # Add token counting middleware
+ token_counter = TokenCountingMiddleware()
+ app.add_middleware(token_counter)
+ logger.info("Token counting middleware added to MCP server")
+
+ return app
+
+# --- Module Imports ---
+from yargitay_mcp_module.client import YargitayOfficialApiClient
+from yargitay_mcp_module.models import (
+ YargitayDetailedSearchRequest, YargitayDocumentMarkdown, CompactYargitaySearchResult,
+ YargitayBirimEnum, CleanYargitayDecisionEntry
+)
+from bedesten_mcp_module.client import BedestenApiClient
+from bedesten_mcp_module.models import (
+ BedestenSearchRequest, BedestenSearchData,
+ BedestenDocumentMarkdown, BedestenCourtTypeEnum
+)
+from bedesten_mcp_module.enums import BirimAdiEnum
+from danistay_mcp_module.client import DanistayApiClient
+from danistay_mcp_module.models import (
+ DanistayKeywordSearchRequest, DanistayDetailedSearchRequest,
+ DanistayDocumentMarkdown, CompactDanistaySearchResult
+)
+from emsal_mcp_module.client import EmsalApiClient
+from emsal_mcp_module.models import (
+ EmsalSearchRequest, EmsalDocumentMarkdown, CompactEmsalSearchResult
+)
+from uyusmazlik_mcp_module.client import UyusmazlikApiClient
+from uyusmazlik_mcp_module.models import (
+ UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown,
+ UyusmazlikBolumEnum, UyusmazlikTuruEnum, UyusmazlikKararSonucuEnum
+)
+from anayasa_mcp_module.client import AnayasaMahkemesiApiClient
+from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient
+from anayasa_mcp_module.unified_client import AnayasaUnifiedClient
+from anayasa_mcp_module.models import (
+ AnayasaNormDenetimiSearchRequest,
+ AnayasaSearchResult,
+ AnayasaDocumentMarkdown,
+ AnayasaBireyselReportSearchRequest,
+ AnayasaBireyselReportSearchResult,
+ AnayasaBireyselBasvuruDocumentMarkdown,
+ AnayasaUnifiedSearchRequest,
+ AnayasaUnifiedSearchResult,
+ AnayasaUnifiedDocumentMarkdown,
+ # Removed enum imports - now using Literal strings in models
+)
+# KIK Module Imports
+from kik_mcp_module.client import KikApiClient
+from kik_mcp_module.models import (
+ KikKararTipi,
+ KikSearchRequest,
+ KikSearchResult,
+ KikDocumentMarkdown
+)
+
+from rekabet_mcp_module.client import RekabetKurumuApiClient
+from rekabet_mcp_module.models import (
+ RekabetKurumuSearchRequest,
+ RekabetSearchResult,
+ RekabetDocument,
+ RekabetKararTuruGuidEnum
+)
+
+from sayistay_mcp_module.client import SayistayApiClient
+from sayistay_mcp_module.models import (
+ GenelKurulSearchRequest, GenelKurulSearchResponse,
+ TemyizKuruluSearchRequest, TemyizKuruluSearchResponse,
+ DaireSearchRequest, DaireSearchResponse,
+ SayistayDocumentMarkdown,
+ SayistayUnifiedSearchRequest, SayistayUnifiedSearchResult,
+ SayistayUnifiedDocumentMarkdown
+)
+from sayistay_mcp_module.enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum
+from sayistay_mcp_module.unified_client import SayistayUnifiedClient
+
+# KVKK Module Imports
+from kvkk_mcp_module.client import KvkkApiClient
+from kvkk_mcp_module.models import (
+ KvkkSearchRequest,
+ KvkkSearchResult,
+ KvkkDocumentMarkdown
+)
+
+# BDDK Module Imports
+from bddk_mcp_module.client import BddkApiClient
+from bddk_mcp_module.models import (
+ BddkSearchRequest,
+ BddkSearchResult,
+ BddkDocumentMarkdown
+)
+
+
+# Create a placeholder app that will be properly initialized after tools are defined
+from fastmcp import FastMCP
+
+# Placeholder app for decorators - will be replaced in create_app() after all tools are defined
+app = FastMCP("Yargı MCP Server Placeholder")
+
+# --- Tool Documentation Resources ---
+@app.resource("docs://tools/yargitay")
+async def get_yargitay_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Yargıtay (Court of Cassation) Tools Documentation
+
+## Court Hierarchy and Position
+Yargıtay is Turkey's highest civil and criminal court. It serves as the final appellate authority and establishes legal precedents for civil and criminal cases.
+
+**Dual API System:**
+- **Primary API (search_yargitay_detailed)**: Official karararama.yargitay.gov.tr
+- **Bedesten API (search_bedesten_unified)**: Unified access to bedesten.adalet.gov.tr (see docs://tools/bedesten_unified)
+
+## Chamber Filtering Options (52 Total)
+
+### Civil Chambers (Hukuk Daireleri)
+- **Civil General Assembly** (Hukuk Genel Kurulu)
+- **1st Civil Chamber** through **23rd Civil Chamber** (23 civil chambers)
+- **Civil Chambers Presidents Board** (Hukuk Daireleri Başkanlar Kurulu)
+
+### Criminal Chambers (Ceza Daireleri)
+- **Criminal General Assembly** (Ceza Genel Kurulu)
+- **1st Criminal Chamber** through **23rd Criminal Chamber** (23 criminal chambers)
+- **Criminal Chambers Presidents Board** (Ceza Daireleri Başkanlar Kurulu)
+
+### General Assemblies
+- **Grand General Assembly** (Büyük Genel Kurulu)
+
+## Search Techniques
+
+### Primary API (search_yargitay_detailed)
+```
+Simple search: "mülkiyet"
+AND operator: "mülkiyet AND tapu"
+OR operator: "mülkiyet OR tapu"
+NOT operator: "mülkiyet NOT satış"
+Wildcard: "mülk*"
+Exact phrase: "\"mülkiyet hakkı\""
+```
+
+### Bedesten API (search_bedesten_unified)
+For detailed usage, see docs://tools/bedesten_unified
+```
+Regular search: phrase="mülkiyet kararı", court_types=["YARGITAYKARARI"]
+Exact phrase: phrase="\"mülkiyet kararı\"", court_types=["YARGITAYKARARI"]
+Date filtering: kararTarihiStart="2024-01-01T00:00:00.000Z"
+```
+
+## Usage Scenarios
+- **Precedent research**: Supreme court decisions on specific topics
+- **Chamber-specific search**: Relevant chambers for specific legal areas
+- **Historical analysis**: Decision trends in specific periods
+- **Jurisprudence tracking**: Changes in legal opinions
+
+## Best Practices
+1. **Use dual APIs**: Try both APIs for maximum coverage
+2. **Chamber filtering**: Select chambers based on relevant legal area
+3. **Exact phrases**: Use "\"term\"" for precise terms in Bedesten API
+4. **Date range**: Focus on last 2-3 years for recent developments
+
+## Common Civil Chambers
+- **1st Civil**: Property, land registry, liens
+- **4th Civil**: Labor law, collective agreements
+- **11th Civil**: Insurance, social security
+- **15th Civil**: Compensation, tort
+- **21st Civil**: Execution and bankruptcy
+
+## Common Criminal Chambers
+- **1st Criminal**: General criminal offenses
+- **8th Criminal**: Economic and commercial crimes
+- **12th Criminal**: Official misconduct
+"""
+
+@app.resource("docs://tools/danistay")
+async def get_danistay_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Danıştay (Council of State) Tools Documentation
+
+## Court Hierarchy and Position
+Danıştay is Turkey's highest administrative court. It makes final decisions on administrative acts and actions.
+
+**Triple API System:**
+- **Keyword API (search_danistay_by_keyword)**: AND/OR/NOT logic
+- **Detailed API (search_danistay_detailed)**: Comprehensive criteria
+- **Bedesten API (search_bedesten_unified)**: Unified access (see docs://tools/bedesten_unified)
+
+## Chamber Filtering Options (27 Total)
+
+### Main Councils
+- **Grand General Assembly** (Büyük Gen.Kur.)
+- **Administrative Cases Council** (İdare Dava Daireleri Kurulu)
+- **Tax Cases Council** (Vergi Dava Daireleri Kurulu)
+- **Precedents Unification Council** (İçtihatları Birleştirme Kurulu)
+
+### Chambers (1-17)
+- **1st Chamber** through **17th Chamber** (Administrative case chambers)
+
+### Military Courts
+- **Military High Administrative Court** (Askeri Yüksek İdare Mahkemesi)
+- **Military High Administrative Court 1st-3rd Chambers**
+
+## Search Techniques
+
+### Keyword API
+```
+AND logic: andKelimeler=["imar", "plan"]
+OR logic: orKelimeler=["iptal", "yürütmeyi durdurma"]
+NOT logic: notKelimeler=["ceza"]
+```
+
+### Detailed API
+```
+Chamber selection: daire="3. Daire"
+Case year: esasYil="2024"
+Decision date: kararTarihiBaslangic="01.01.2024"
+Legislation: mevzuatId=123
+```
+
+### Bedesten API
+```
+Regular: phrase="idari işlem"
+Exact: phrase="\"idari işlem\""
+Date: kararTarihiStart="2024-01-01T00:00:00.000Z"
+```
+
+## Usage Scenarios
+- **Administrative law research**: Public administration decisions
+- **Tax law**: Financial matters and tax disputes
+- **Urban planning law**: City planning and building permits
+- **Personnel law**: Civil servant rights
+
+## Common Chamber Specializations
+- **1st Chamber**: Municipal, urban planning, environment
+- **2nd Chamber**: Tax, customs, financial
+- **3rd Chamber**: Personnel, personal rights
+- **5th Chamber**: Administrative fines
+- **8th Chamber**: Higher education, education
+- **10th Chamber**: Health, social security
+
+## Best Practices
+1. **Triple API**: Use all three APIs for maximum coverage
+2. **Chamber selection**: Choose specialized chambers by subject area
+3. **Mevzuat bağlantısı**: İlgili kanun/tüzükle filtreleme
+4. **Kesin terim**: İdari hukuk terminolojisi için exact search
+"""
+
+@app.resource("docs://tools/constitutional_court")
+async def get_constitutional_court_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Anayasa Mahkemesi (Constitutional Court) Tools Documentation
+
+## Court Position
+Constitutional Court is Turkey's highest judicial body. It has two main functions:
+
+### 1. Norm Control (Norm Control)
+**Tool**: search_anayasa_norm_denetimi_decisions
+- Reviews constitutional compliance of laws and regulations
+- Abstract and concrete norm control
+
+### 2. Individual Application (Individual Application)
+**Tool**: search_anayasa_bireysel_basvuru_report
+- Citizens' fundamental rights violation applications
+- Turkey's human rights protection mechanism
+
+## Norm Control Features
+
+### Comprehensive Filtering
+- **Application type**: Annulment, Objection, Other
+- **Applicant**: President, Parliament, Courts
+- **Legislation type**: Law, Decree, Regulation, Rules of procedure
+- **Result type**: Annulment, Rejection, Partial annulment
+
+### Advanced Search
+- **Member names**: Full names of participating justices
+- **Rapporteur**: Case rapporteur
+- **Dissenting opinion**: Minority opinion, different view
+- **Press release**: Important decisions
+
+## Bireysel Başvuru Özellikleri
+
+### Temel Haklar Kategorileri
+- **Yaşam hakkı**: Ölüm olayları, güvenlik
+- **Adil yargılanma**: Süre, tarafsızlık, duruşma hakkı
+- **İfade özgürlüğü**: Basın, düşünce, akademik özgürlük
+- **Din özgürlüğü**: İbadet, vicdan özgürlüğü
+- **Mülkiyet hakkı**: Kamulaştırma, tapu
+- **Özel hayat**: Gizlilik, aile hayatı
+
+### Başvuru Süreci
+- **Yurtiçi yollar**: Önce mahkeme kararı gerekli
+- **Süre sınırı**: 30 gün (60 gün istisnai)
+- **Kabul edilebilirlik**: Ön inceleme kriterleri
+
+## Paginated Content (5,000 characters)
+Her iki tool da sayfalanmış Markdown döndürür:
+- **page_number**: Sayfa numarası (1'den başlar)
+- **total_pages**: Toplam sayfa sayısı
+- **current_page**: Mevcut sayfa
+
+## Usage Scenarios
+
+### Norm Denetimi
+- **Kanun anayasaya uygunluk**: Yeni çıkan kanunların kontrolü
+- **Mahkeme iptali**: Kanunun belirli maddeleri
+- **Mevzuat uyum**: Anayasa değişikliği sonrası
+
+### Bireysel Başvuru
+- **İnsan hakları araştırması**: AİHM öncesi iç hukuk
+- **Temel hak ihlalleri**: Sistematik ihlal tespiti
+- **Emsal karar**: Benzer davalar için içtihat
+
+## Parameter Details
+### search_anayasa_norm_denetimi_decisions
+- **keywords_all**: Keywords for AND logic (all must be present)
+- **keywords_any**: Keywords for OR logic (any can be present)
+- **keywords_exclude**: Keywords to exclude from results
+- **period**: Constitutional period - "ALL", "1" (1961 Constitution), "2" (1982 Constitution)
+- **case_number_esas**: Case registry number (e.g., '2023/123')
+- **decision_number_karar**: Decision number (e.g., '2023/456')
+- **first_review_date_start/end**: First review date range (DD/MM/YYYY)
+- **decision_date_start/end**: Decision date range (DD/MM/YYYY)
+- **application_type**: "ALL", "1" (İptal), "2" (İtiraz), "3" (Diğer)
+- **applicant_general_name**: General applicant name
+- **applicant_specific_name**: Specific applicant name
+- **official_gazette_date_start/end**: Official Gazette date range
+- **official_gazette_number_start/end**: Official Gazette number range
+- **has_press_release**: "ALL", "0" (No), "1" (Yes)
+- **has_dissenting_opinion**: "ALL", "0" (No), "1" (Yes)
+- **has_different_reasoning**: "ALL", "0" (No), "1" (Yes)
+- **attending_members_names**: List of attending members' exact names
+- **rapporteur_name**: Rapporteur's exact name
+- **norm_type**: Type of reviewed norm (law, decree, regulation, etc.)
+- **norm_id_or_name**: Number or name of the norm
+- **norm_article**: Article number of the norm
+- **review_outcomes**: List of review outcomes
+- **reason_for_final_outcome**: Main reason for decision outcome
+- **basis_constitution_article_numbers**: Supporting Constitution article numbers
+- **results_per_page**: Results per page (10, 20, 30, 40, 50)
+- **page_to_fetch**: Page number to fetch
+- **sort_by_criteria**: Sort criteria ('KararTarihi', 'YayinTarihi', 'Toplam')
+
+### search_anayasa_bireysel_basvuru_report
+- **keywords**: Keywords for AND logic (all must be present)
+- **page_to_fetch**: Page number for the report (default: 1)
+
+### Document Tools
+- **document_url**: URL path or full URL of the decision
+- **page_number**: Page number for paginated content (1-indexed, default: 1)
+
+## Best Practices
+1. **Norm control önce**: Kanun iptal edilmiş mi kontrol
+2. **Bireysel başvuru ikinci**: Kişisel hak ihlalleri için
+3. **Tarih aralığı**: Anayasa değişiklikleri sonrası dönemler
+4. **Anahtar kelime kombinasyonu**: Temel hak + konu alanı
+5. **Sayfa yönetimi**: Uzun kararlarda sayfa sayfa okuyun
+"""
+
+@app.resource("docs://tools/emsal")
+async def get_emsal_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Emsal (UYAP Precedent System) Tools Documentation
+
+## System Position
+Central precedent decision system providing access to all court decisions through the UYAP system.
+
+## Court Options
+- **Yargıtay**: First and second instance courts
+- **Danıştay**: Administrative court decisions
+- **Other**: Regional courts of justice, civil courts
+
+## Advanced Filtering Features
+- **Court type**: Civil, criminal, administrative
+- **Case/Decision number**: File tracking system
+- **Date range**: Flexible date selection
+- **Content search**: Keyword search within decision text
+
+## Usage Scenarios
+- **Kapsamlı emsal**: Tüm mahkeme seviyelerinden karar toplama
+- **Güncel içtihat**: En son hukuki gelişmeler
+- **Cross-reference**: Farklı mahkeme görüşlerini karşılaştırma
+
+## Best Practices
+1. **Spesifik terimler**: Hukuki terminoloji kullanın
+2. **Geniş arama**: Önce genel, sonra spesifik
+3. **Tarih stratejisi**: Mevzuat değişiklikleri dikkate alın
+4. **Cross-platform**: Aynı konuyu farklı mahkemelerde arayın
+"""
+
+@app.resource("docs://tools/uyusmazlik")
+async def get_uyusmazlik_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Uyuşmazlık Mahkemesi Tools Documentation
+
+## Court Position
+Adli ve idari yargı arasındaki görev uyuşmazlıklarını çözen özel yetkili mahkeme.
+
+## Dispute Types
+- **Görev uyuşmazlığı**: Hangi mahkeme bakacak konusunda anlaşmazlık
+- **Hüküm uyuşmazlığı**: Çelişkili mahkeme kararları
+- **Yetki uyuşmazlığı**: Yerel yetki sorunları
+
+## Form-Based Search Criteria
+- **Karar türü**: Müspet, menfi, hüküm uyuşmazlığı
+- **Taraf mahkemeler**: Adli-idari yargı organları
+- **Konu alanı**: Hukuk dalı bazlı filtreleme
+- **Tarih aralığı**: Karar tarihi seçimi
+
+## Usage Scenarios
+- **Yargı türü belirleme**: Hangi mahkemenin yetkili olduğu
+- **Çelişkili kararlar**: Farklı mahkeme kararları arasındaki uyuşmazlık
+- **Yetki sorunları**: Mahkeme yetkisi tartışmaları
+
+## Best Practices
+1. **Net kriterler**: Arama kriterlerini spesifik tutun
+2. **Taraf bilgisi**: Uyuşmazlık taraflarını belirtin
+3. **Konu odaklı**: İlgili hukuk dalını seçin
+"""
+
+@app.resource("docs://tools/kik")
+async def get_kik_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# KİK (Kamu İhale Kurumu) Tools Documentation
+
+## Kurum Konumu
+Kamu ihale uyuşmazlıklarının ilk ve son merci çözüm organı. Kamu İhale Kanunu kapsamındaki tüm ihaleler için yetkili.
+
+## Decision Types
+- **Uyuşmazlık**: İhale süreç itirazları
+- **Düzenleyici**: Mevzuat ve uygulama kararları
+- **Mahkeme**: Mahkeme kararlarının uygulanması
+
+## Filtreleme Seçenekleri
+- **Karar numarası**: 2024/UH.II-1766 formatında
+- **Tarih aralığı**: Karar tarihi filtreleme
+- **İhaleyi yapan idare**: Bakanlık, belediye, hastane, üniversite
+- **Başvuru sahibi**: Şirket, firma adı
+- **İhale konusu**: Mal, hizmet, yapım işi
+
+## Sayfalanmış İçerik Özelliği
+5.000 karakterlik sayfalar halinde Markdown formatında sunulur.
+
+## Usage Scenarios
+- **İhale hukuku**: Kamu alımları, süreç kuralları
+- **Başvuru hazırlığı**: Benzer davalar, emsal kararlar
+- **Mevzuat yorumu**: Kamu İhale Kanunu uygulaması
+- **İtiraz stratejisi**: Başarılı itiraz örnekleri
+
+## İhale Süreç Aşamaları
+1. **İhale öncesi**: İlan, şartname hazırlığı
+2. **İhale aşaması**: Teklif verme, değerlendirme
+3. **İhale sonrası**: Sonuç bildirimi, itirazlar
+4. **Sözleşme**: İmza, uygulama
+
+## Parameter Details
+### search_kik_decisions
+- **karar_tipi**: Decision type - "rbUyusmazlik" (disputes), "rbDuzenleyici" (regulatory), "rbMahkeme" (court)
+- **karar_no**: Decision number (e.g., '2024/UH.II-1766')
+- **karar_tarihi_baslangic**: Decision start date (DD.MM.YYYY format)
+- **karar_tarihi_bitis**: Decision end date (DD.MM.YYYY format)
+- **basvuru_sahibi**: Applicant name/company
+- **ihaleyi_yapan_idare**: Procuring entity (ministry, municipality, etc.)
+- **basvuru_konusu_ihale**: Tender subject/description
+- **karar_metni**: Text search with operators: +word (AND), -word (exclude)
+- **yil**: Decision year
+- **resmi_gazete_tarihi**: Official Gazette date (DD.MM.YYYY)
+- **resmi_gazete_sayisi**: Official Gazette number
+- **page**: Results page number
+
+### get_kik_document_markdown
+- **karar_id**: Base64 encoded decision identifier from search results
+- **page_number**: Page number for paginated content (1-indexed, default: 1)
+
+## Best Practices
+1. **İhale türü**: Açık, belli istekliler arası, pazarlık
+2. **Süreç aşaması**: Hangi aşamada sorun olduğu
+3. **Hukuki dayanak**: İlgili KİK kanun maddesi
+4. **Sayfa yönetimi**: Uzun kararları bölümler halinde okuyun
+"""
+
+@app.resource("docs://tools/rekabet")
+async def get_rekabet_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Rekabet Kurumu (Competition Authority) Tools Documentation
+
+## Kurum Konumu
+Rekabet hukuku ihlallerini inceleyen ve ceza veren idari otorite. Rekabet Kanunu kapsamında yetkili.
+
+## Decision Types
+- **Birleşme ve Devralma**: Şirket satın almaları, füzyonlar
+- **Rekabet İhlali**: Anlaşma, hakim durum kötüye kullanımı
+- **Menfi Tespit ve Muafiyet**: İhlal yok kararları, muafiyetler
+- **Özelleştirme**: Kamu şirketleri satışı onayları
+
+## Filtreleme Özellikleri
+- **PDF metin arama**: Tam metin içinde kelime arama
+- **Karar türü**: Spesifik kategori seçimi
+- **Tarih aralığı**: 1997'den günümüze karar arşivi
+- **Sektör**: Telekomünikasyon, bankacılık, enerji, perakende
+
+## Rekabet Hukuku Temel Kavramları
+- **Hakim durum**: Pazar gücü
+- **Kartel**: Fiyat anlaşması
+- **Dikey anlaşmalar**: Tedarikci-bayi ilişkileri
+- **Konsantrasyon**: Birleşme işlemleri
+
+## Usage Scenarios
+- **Antitrust araştırması**: Tekelleşme, kartel soruşturmaları
+- **Birleşme incelemesi**: M&A transaction değerlendirmesi
+- **Sektör analizi**: Belirli pazarlardaki rekabet durumu
+- **Ceza hesaplama**: İhlal cezası örnekleri
+
+## Sektörel Uzmanlık Alanları
+1. **Telekomünikasyon**: Operatör rekabeti
+2. **Enerji**: Elektrik, doğalgaz piyasası
+3. **Finans**: Bankacılık, sigorta
+4. **Perakende**: Zincir mağazalar
+5. **İnşaat**: Müteahhitlik sektörü
+
+## Parameter Details
+### search_rekabet_kurumu_decisions
+- **sayfaAdi**: Search in decision title (Başlık)
+- **YayinlanmaTarihi**: Publication date (DD.MM.YYYY format)
+- **PdfText**: Search text. For exact phrases use double quotes: "vertical agreement"
+- **KararTuru**: Decision type - "Birleşme ve Devralma", "Rekabet İhlali", etc.
+- **KararSayisi**: Decision number (Karar Sayısı)
+- **KararTarihi**: Decision date (DD.MM.YYYY format)
+- **page**: Page number for results list
+
+### get_rekabet_kurumu_document
+- **karar_id**: GUID from search results
+- **page_number**: Requested page for Markdown content (1-indexed, default: 1)
+
+## Best Practices
+1. **Sektör odaklı**: İlgili sektörde arama yapın
+2. **Karar türü seçimi**: İhtiyacınıza uygun kategori
+3. **Güncel mevzuat**: Mevzuat değişiklikleri takibi
+4. **Sayfa yönetimi**: Uzun analizleri bölümler halinde
+"""
+
+@app.resource("docs://tools/bedesten_unified")
+async def get_bedesten_unified_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Bedesten API Mahkemeleri Tools Documentation
+
+## Bedesten API Sistemi
+bedesten.adalet.gov.tr üzerinden Türk adalet sistemi hiyerarşisindeki mahkemelere erişim.
+
+## Mahkeme Hiyerarşisi Kapsamı
+
+### 1. Yerel Hukuk Mahkemeleri (Local Civil Courts)
+**Tool**: search_yerel_hukuk_bedesten
+- **Konum**: İlk derece mahkemeler
+- **Yetki**: Hukuki uyuşmazlıklar (sözleşme, tazminat, mülkiyet)
+- **Önem**: Toplumun günlük hukuki sorunları
+
+### 2. İstinaf Hukuk Mahkemeleri (Civil Courts of Appeals)
+**Tool**: search_istinaf_hukuk_bedesten
+- **Konum**: Orta derece (Yerel -> İstinaf -> Yargıtay)
+- **Yetki**: Yerel mahkeme kararlarına itiraz
+- **Önem**: Temyiz öncesi son kontrol
+
+### 3. Kanun Yararına Bozma (KYB)
+**Tool**: search_kyb_bedesten
+- **Konum**: Olağanüstü kanun yolu
+- **Başvuru sahibi**: Cumhuriyet Başsavcılığı
+- **Amaç**: Hukuka aykırı kararları düzeltme
+- **Özellik**: Sanık aleyhine olsa bile hukuk yararına
+
+## Ortak Bedesten API Özellikleri
+
+### Tarih Filtreleme (ISO 8601)
+```
+Başlangıç: kararTarihiStart="2024-01-01T00:00:00.000Z"
+Bitiş: kararTarihiEnd="2024-12-31T23:59:59.999Z"
+Tek gün: "2024-06-25T00:00:00.000Z" - "2024-06-25T23:59:59.999Z"
+```
+
+### Kesin Cümle Arama
+```
+Normal: phrase="sözleşme ihlali" (kelimeler ayrı ayrı)
+Kesin: phrase="\"sözleşme ihlali\"" (tam cümle)
+```
+
+### Sayfalama
+- **pageSize**: 1-100 arası sonuç sayısı
+- **pageNumber**: Sayfa numarası (1'den başlar)
+
+## Mahkeme Özellikleri
+
+### Yerel Hukuk Mahkemeleri
+**Yaygın Dava Türleri**:
+- Sözleşme ihlali davaları
+- Tazminat talepleri
+- Mülkiyet uyuşmazlıkları
+- Aile hukuku (boşanma, nafaka)
+- Ticari uyuşmazlıklar (küçük-orta ölçek)
+
+**Kullanım Senaryoları**:
+- Günlük hukuki sorunlar
+- Vatandaş hakları
+- Ticaret hukuku temelleri
+- İcra takipleri
+
+### İstinaf Hukuk Mahkemeleri
+**İnceleme Kapsamı**:
+- Yerel mahkeme kararlarının kontrolü
+- Hukuki ve maddi hata arayışı
+- Yeniden yargılama (sınırlı)
+
+**Kullanım Senaryoları**:
+- Temyiz stratejisi gelişitirme
+- İstinaf mahkemesi içtihatları
+- Yerel-üst mahkeme uyumu analizi
+
+### Kanun Yararına Bozma (KYB)
+**Başvuru Koşulları**:
+- Kesinleşmiş mahkeme kararı
+- Hukuka açık aykırılık
+- Cumhuriyet Başsavcılığı başvurusu
+- Sanık aleyhine sonuç doğurmama
+
+**Kullanım Senaryoları**:
+- Sistematik hukuki hatalar
+- İçtihat birliğini sağlama
+- Hukuk güvenliği
+- Nadir ve özel hukuki durumlar
+
+## Arama Stratejileri
+
+### Hiyerarşik Arama
+```
+1. Yerel mahkeme -> Gündelik sorunlar
+2. İstinaf -> Kompleks yorumlar
+3. KYB -> İstisnai hukuki durumlar
+```
+
+### Kesin Terim Kullanımı
+```
+Yerel: "\"sözleşme ihlali\""
+İstinaf: "\"temyiz incelemesi\""
+KYB: "\"kanun yararına bozma\""
+```
+
+### Tarih Stratejisi
+- **Son 2 yıl**: Güncel içtihat
+- **5-10 yıl**: Yerleşik görüşler
+- **Mevzuat değişikliği sonrası**: Yeni uygulamalar
+
+## Best Practices
+1. **Hiyerarşi takibi**: Alt mahkemeden üst mahkemeye
+2. **Kesin cümle**: Hukuki terimler için "\"terim\""
+3. **Tarih aralığı**: İlgili mevzuat dönemleri
+4. **Cross-reference**: Aynı konuyu farklı seviyelerde
+5. **Minimal sonuç**: KYB çok nadir, az sonuç beklenir
+
+## Document ID Formatı
+Tüm Bedesten mahkemeleri documentId döndürür:
+- **Format**: Alfanumerik string
+- **Kullanım**: get_*_bedesten_document_markdown fonksiyonları
+- **İçerik**: HTML/PDF -> Markdown conversion
+"""
+
+@app.resource("docs://tools/sayistay")
+async def get_sayistay_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# Sayıştay (Court of Accounts) Tools Documentation
+
+## Sayıştay'ın Konumu
+Türkiye'nin en üst mali denetim organı. Kamu kaynaklarının kullanımını denetler ve mali disiplini sağlar.
+
+## Üç Tür Karar Sistemi
+
+### 1. Genel Kurul Kararları (Interpretive Rulings)
+**Tool**: search_sayistay_genel_kurul
+- **İşlev**: Mali mevzuat yorumlama
+- **Kapsam**: 2006-2024 yılları arası
+- **Özellik**: Bağlayıcı yorumlar
+
+**Filtreleme Seçenekleri**:
+- **Karar numarası**: Spesifik karar arama
+- **Tarih aralığı**: Başlangıç-bitiş tarihleri
+- **Karar tamamı**: Tam metin arama (400 karakter)
+
+### 2. Temyiz Kurulu Kararları (Appeals Board)
+**Tool**: search_sayistay_temyiz_kurulu
+- **İşlev**: Daire kararlarına itiraz incelemesi
+- **8 Daire Filtreleme**: Uzmanlık alanlarına göre
+
+**Daire Uzmanlaşmaları**:
+- **1. Daire**: Genel bütçeli idareler
+- **2. Daire**: Mahalli idareler
+- **3. Daire**: Sosyal güvenlik kurumları
+- **4. Daire**: KİT ve bağlı ortaklıklar
+- **5. Daire**: Düzenleyici kuruluşlar
+- **6. Daire**: Vakıflar, dernekler
+- **7. Daire**: Üniversiteler, eğitim
+- **8. Daire**: Yatırım projeleri
+
+**Filtreleme Seçenekleri**:
+- **İdare türü**: Bakanlık, belediye, üniversite, KİT
+- **Temyiz karar**: Tam metin arama
+- **Konu sınıflandırması**: Harcama, gelir, taşınır-taşınmaz
+
+### 3. Daire Kararları (Chamber Decisions)
+**Tool**: search_sayistay_daire
+- **İşlev**: İlk derece denetim bulguları
+- **8 Daire**: Aynı uzmanlaşma alanları
+
+**Filtreleme Seçenekleri**:
+- **Yargılama dairesi**: 1-8 arası daire seçimi
+- **Hesap yılı**: Mali yıl bazlı
+- **Web karar metni**: İçerik arama
+
+## Ortak Özellikler
+
+### Sayfalanmış Markdown
+Tüm Sayıştay belgeleri sayfalanmış format:
+- **5.000 karakter** per sayfa
+- **page_number**: Sayfa numarası
+- **total_pages**: Toplam sayfa
+
+### Tarih Aralığı Desteği
+- **Genel Kurul**: 2006-2024 (18 yıl)
+- **Temyiz/Daire**: Mevcut veriler üzerinde
+
+## Usage Scenarios
+
+### Mali Mevzuat Araştırması
+```
+Genel Kurul -> Hukuki yorum
+Temyiz -> Uygulama detayları
+Daire -> Spesifik örnekler
+```
+
+### Kamu Mali Yönetimi
+- **Bütçe uygulama**: Harcama usulleri
+- **İhale süreçleri**: Kamu alımları denetimi
+- **Personel giderleri**: Özlük hakları mali boyutu
+- **Yatırım projeleri**: Büyük ölçekli projeler
+
+### Kurumsal Denetim
+- **KİT yönetimi**: Kamu iktisadi teşebbüsleri
+- **Belediye maliyesi**: Yerel yönetim harcamaları
+- **Üniversite bütçesi**: Yükseköğretim mali yönetimi
+- **Sosyal güvenlik**: SGK, Bağ-Kur mali işlemleri
+
+## Arama Stratejileri
+
+### Hiyerarşik Yaklaşım
+1. **Genel Kurul**: Konunun hukuki çerçevesi
+2. **Temyiz**: Tartışmalı uygulamalar
+3. **Daire**: Günlük uygulama örnekleri
+
+### Daire Bazlı Strateji
+```
+Mali konu -> İlgili daire seçimi -> Derinlemesine arama
+Örnek: KİT mali sorunları -> 4. Daire
+```
+
+### Tarih Odaklı Strateji
+- **Son 2 yıl**: Güncel uygulamalar
+- **5 yıl**: Yerleşik görüşler
+- **2006-2024**: Tarihsel gelişim
+
+## Best Practices
+1. **Daire uzmanlaşması**: İlgili kuruma uygun daire
+2. **Hiyerarşik sıralama**: Genel Kurul -> Temyiz -> Daire
+3. **Mali dönem**: Bütçe yılları bazında arama
+4. **Teknik terimler**: Mali mevzuat terminolojisi
+5. **Cross-reference**: Farklı seviyelerden görüş karşılaştırma
+
+## İdare Türü Kodları
+- **1**: Genel bütçeli
+- **2**: Özel bütçeli
+- **3**: Düzenleyici kuruluşlar
+- **4**: Mahalli idareler
+- **5**: Sosyal güvenlik
+- **6**: KİT
+- **7**: Vakıf/dernek
+- **8**: Diğer kamu kuruluşları
+"""
+
+@app.resource("docs://tools/kvkk")
+async def get_kvkk_tools_documentation() -> str:
+ """Get document content as Markdown."""
+ return """
+# KVKK (Personal Data Protection Authority) Tools
+
+## Overview
+KVKK (Kişisel Verilerin Korunması Kurulu) - Turkey's GDPR equivalent authority.
+
+## Search Features
+- **Brave Search API**: Searches kvkk.gov.tr with Turkish terms
+- **Site-targeted**: Auto `site:kvkk.gov.tr "karar özeti"`
+- **Pagination**: page and pageSize parameters
+- **5,000-char pages**: Paginated Markdown documents
+
+## Common Search Terms
+**Violations**: "veri ihlali", "açık rıza", "idari para cezası"
+**Compliance**: "GDPR uyum", "veri koruma", "güvenlik tedbirleri"
+**Sectors**: "e-ticaret", "bankacılık", "sağlık", "mobil uygulama"
+
+## Key Decision Types
+- **Fines**: Data breaches, consent violations
+- **Compliance**: GDPR alignment, corporate policies
+- **Breach notifications**: 24-hour rule violations
+
+## Usage Tips
+1. Use Turkish legal terms for best results
+2. Combine sector + violation type searches
+3. Use page_number for long decisions
+4. Focus on recent 2-3 years for current practices
+"""
+
+
+# --- API Client Instances ---
+yargitay_client_instance = YargitayOfficialApiClient()
+danistay_client_instance = DanistayApiClient()
+emsal_client_instance = EmsalApiClient()
+uyusmazlik_client_instance = UyusmazlikApiClient()
+anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
+anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
+anayasa_unified_client_instance = AnayasaUnifiedClient()
+kik_client_instance = KikApiClient()
+rekabet_client_instance = RekabetKurumuApiClient()
+bedesten_client_instance = BedestenApiClient()
+sayistay_client_instance = SayistayApiClient()
+sayistay_unified_client_instance = SayistayUnifiedClient()
+kvkk_client_instance = KvkkApiClient()
+bddk_client_instance = BddkApiClient()
+
+
+KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
+ "": RekabetKararTuruGuidEnum.TUMU, # Keep for backward compatibility
+ "ALL": RekabetKararTuruGuidEnum.TUMU, # Map "ALL" to TUMU
+ "Birleşme ve Devralma": RekabetKararTuruGuidEnum.BIRLESME_DEVRALMA,
+ "Diğer": RekabetKararTuruGuidEnum.DIGER,
+ "Menfi Tespit ve Muafiyet": RekabetKararTuruGuidEnum.MENFI_TESPIT_MUAFIYET,
+ "Özelleştirme": RekabetKararTuruGuidEnum.OZELLESTIRME,
+ "Rekabet İhlali": RekabetKararTuruGuidEnum.REKABET_IHLALI,
+}
+
+# --- MCP Tools for Yargitay ---
+"""
+@app.tool(
+ description="Search Yargıtay decisions with 52 chamber filtering and advanced operators",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_yargitay_detailed(
+ arananKelime: str = Field("", description="Turkish search keyword. Supports +required -excluded \"exact phrase\" operators"),
+ birimYrgKurulDaire: str = Field("ALL", description="Chamber selection (52 options: Civil/Criminal chambers, General Assemblies)"),
+ esasYil: str = Field("", description="Case year for 'Esas No'."),
+ esasIlkSiraNo: str = Field("", description="Starting sequence number for 'Esas No'."),
+ esasSonSiraNo: str = Field("", description="Ending sequence number for 'Esas No'."),
+ kararYil: str = Field("", description="Decision year for 'Karar No'."),
+ kararIlkSiraNo: str = Field("", description="Starting sequence number for 'Karar No'."),
+ kararSonSiraNo: str = Field("", description="Ending sequence number for 'Karar No'."),
+ baslangicTarihi: str = Field("", description="Start date for decision search (DD.MM.YYYY)."),
+ bitisTarihi: str = Field("", description="End date for decision search (DD.MM.YYYY)."),
+ # pageSize: int = Field(10, ge=1, le=10, description="Number of results per page."),
+ pageNumber: int = Field(1, ge=1, description="Page number to retrieve.")
+) -> CompactYargitaySearchResult:
+ # Search Yargıtay decisions using primary API with 52 chamber filtering and advanced operators.
+
+ # Convert "ALL" to empty string for API compatibility
+ if birimYrgKurulDaire == "ALL":
+ birimYrgKurulDaire = ""
+
+ pageSize = 10 # Default value
+
+ search_query = YargitayDetailedSearchRequest(
+ arananKelime=arananKelime,
+ birimYrgKurulDaire=birimYrgKurulDaire,
+ esasYil=esasYil,
+ esasIlkSiraNo=esasIlkSiraNo,
+ esasSonSiraNo=esasSonSiraNo,
+ kararYil=kararYil,
+ kararIlkSiraNo=kararIlkSiraNo,
+ kararSonSiraNo=kararSonSiraNo,
+ baslangicTarihi=baslangicTarihi,
+ bitisTarihi=bitisTarihi,
+ siralama="3",
+ siralamaDirection="desc",
+ pageSize=pageSize,
+ pageNumber=pageNumber
+ )
+
+ logger.info(f"Tool 'search_yargitay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
+ try:
+ api_response = await yargitay_client_instance.search_detailed_decisions(search_query)
+ if api_response and api_response.data and api_response.data.data:
+ # Convert to clean decision entries without arananKelime field
+ clean_decisions = [
+ CleanYargitayDecisionEntry(
+ id=decision.id,
+ daire=decision.daire,
+ esasNo=decision.esasNo,
+ kararNo=decision.kararNo,
+ kararTarihi=decision.kararTarihi,
+ document_url=decision.document_url
+ )
+ for decision in api_response.data.data
+ ]
+ return CompactYargitaySearchResult(
+ decisions=clean_decisions,
+ total_records=api_response.data.recordsTotal if api_response.data else 0,
+ requested_page=search_query.pageNumber,
+ page_size=search_query.pageSize)
+ logger.warning("API response for Yargitay search did not contain expected data structure.")
+ return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_yargitay_detailed'.")
+ raise
+
+@app.tool(
+ description="Get Yargıtay decision text in Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def get_yargitay_document_markdown(id: str) -> YargitayDocumentMarkdown:
+ # Get Yargıtay decision text as Markdown. Use ID from search results.
+ logger.info(f"Tool 'get_yargitay_document_markdown' called for ID: {id}")
+ if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string.")
+ try:
+ return await yargitay_client_instance.get_decision_document_as_markdown(id)
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_yargitay_document_markdown'.")
+ raise
+"""
+
+# --- MCP Tools for Danistay ---
+"""
+@app.tool(
+ description="Search Danıştay decisions with keyword logic (AND/OR/NOT operators)",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_danistay_by_keyword(
+ andKelimeler: List[str] = Field(default_factory=list, description="Keywords for AND logic, e.g., ['word1', 'word2']"),
+ orKelimeler: List[str] = Field(default_factory=list, description="Keywords for OR logic."),
+ notAndKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT AND logic."),
+ notOrKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT OR logic."),
+ pageNumber: int = Field(1, ge=1, description="Page number."),
+ # pageSize: int = Field(10, ge=1, le=10, description="Results per page.")
+) -> CompactDanistaySearchResult:
+ # Search Danıştay decisions with keyword logic.
+
+ pageSize = 10 # Default value
+
+ search_query = DanistayKeywordSearchRequest(
+ andKelimeler=andKelimeler,
+ orKelimeler=orKelimeler,
+ notAndKelimeler=notAndKelimeler,
+ notOrKelimeler=notOrKelimeler,
+ pageNumber=pageNumber,
+ pageSize=pageSize
+ )
+
+ logger.info(f"Tool 'search_danistay_by_keyword' called.")
+ try:
+ api_response = await danistay_client_instance.search_keyword_decisions(search_query)
+ if api_response.data:
+ return CompactDanistaySearchResult(
+ decisions=api_response.data.data,
+ total_records=api_response.data.recordsTotal,
+ requested_page=search_query.pageNumber,
+ page_size=search_query.pageSize)
+ logger.warning("API response for Danistay keyword search did not contain expected data structure.")
+ return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_danistay_by_keyword'.")
+ raise
+
+@app.tool(
+ description="Search Danıştay decisions with detailed criteria (chamber selection, case numbers)",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_danistay_detailed(
+ daire: str = Field("", description="Chamber/Department name (e.g., '1. Daire')."),
+ esasYil: str = Field("", description="Case year for 'Esas No'."),
+ esasIlkSiraNo: str = Field("", description="Starting sequence for 'Esas No'."),
+ esasSonSiraNo: str = Field("", description="Ending sequence for 'Esas No'."),
+ kararYil: str = Field("", description="Decision year for 'Karar No'."),
+ kararIlkSiraNo: str = Field("", description="Starting sequence for 'Karar No'."),
+ kararSonSiraNo: str = Field("", description="Ending sequence for 'Karar No'."),
+ baslangicTarihi: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
+ bitisTarihi: str = Field("", description="End date for decision (DD.MM.YYYY)."),
+ mevzuatNumarasi: str = Field("", description="Legislation number."),
+ mevzuatAdi: str = Field("", description="Legislation name."),
+ madde: str = Field("", description="Article number."),
+ pageNumber: int = Field(1, ge=1, description="Page number."),
+ # pageSize: int = Field(10, ge=1, le=10, description="Results per page.")
+) -> CompactDanistaySearchResult:
+ # Search Danıştay decisions with detailed filtering.
+
+ pageSize = 10 # Default value
+
+ search_query = DanistayDetailedSearchRequest(
+ daire=daire,
+ esasYil=esasYil,
+ esasIlkSiraNo=esasIlkSiraNo,
+ esasSonSiraNo=esasSonSiraNo,
+ kararYil=kararYil,
+ kararIlkSiraNo=kararIlkSiraNo,
+ kararSonSiraNo=kararSonSiraNo,
+ baslangicTarihi=baslangicTarihi,
+ bitisTarihi=bitisTarihi,
+ mevzuatNumarasi=mevzuatNumarasi,
+ mevzuatAdi=mevzuatAdi,
+ madde=madde,
+ siralama="3",
+ siralamaDirection="desc",
+ pageNumber=pageNumber,
+ pageSize=pageSize
+ )
+
+ logger.info(f"Tool 'search_danistay_detailed' called.")
+ try:
+ api_response = await danistay_client_instance.search_detailed_decisions(search_query)
+ if api_response.data:
+ return CompactDanistaySearchResult(
+ decisions=api_response.data.data,
+ total_records=api_response.data.recordsTotal,
+ requested_page=search_query.pageNumber,
+ page_size=search_query.pageSize)
+ logger.warning("API response for Danistay detailed search did not contain expected data structure.")
+ return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_danistay_detailed'.")
+ raise
+
+@app.tool(
+ description="Get Danıştay decision text in Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def get_danistay_document_markdown(id: str) -> DanistayDocumentMarkdown:
+ # Get Danıştay decision text as Markdown. Use ID from search results.
+ logger.info(f"Tool 'get_danistay_document_markdown' called for ID: {id}")
+ if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string for Danıştay.")
+ try:
+ return await danistay_client_instance.get_decision_document_as_markdown(id)
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_danistay_document_markdown'.")
+ raise
+"""
+
+# --- MCP Tools for Emsal ---
+@app.tool(
+ description="Search Emsal precedent decisions with detailed criteria",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_emsal_detailed_decisions(
+ keyword: str = Field("", description="Keyword to search."),
+ selected_bam_civil_court: str = Field("", description="Selected BAM Civil Court."),
+ selected_civil_court: str = Field("", description="Selected Civil Court."),
+ selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Selected Regional Civil Chambers."),
+ case_year_esas: str = Field("", description="Case year for 'Esas No'."),
+ case_start_seq_esas: str = Field("", description="Starting sequence for 'Esas No'."),
+ case_end_seq_esas: str = Field("", description="Ending sequence for 'Esas No'."),
+ decision_year_karar: str = Field("", description="Decision year for 'Karar No'."),
+ decision_start_seq_karar: str = Field("", description="Starting sequence for 'Karar No'."),
+ decision_end_seq_karar: str = Field("", description="Ending sequence for 'Karar No'."),
+ start_date: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
+ end_date: str = Field("", description="End date for decision (DD.MM.YYYY)."),
+ sort_criteria: str = Field("1", description="Sorting criteria (e.g., 1: Esas No)."),
+ 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:
+ """Search Emsal precedent decisions with detailed criteria."""
+
+ page_size = 10 # Default value
+
+ search_query = EmsalSearchRequest(
+ keyword=keyword,
+ selected_bam_civil_court=selected_bam_civil_court,
+ selected_civil_court=selected_civil_court,
+ selected_regional_civil_chambers=selected_regional_civil_chambers,
+ case_year_esas=case_year_esas,
+ case_start_seq_esas=case_start_seq_esas,
+ case_end_seq_esas=case_end_seq_esas,
+ decision_year_karar=decision_year_karar,
+ decision_start_seq_karar=decision_start_seq_karar,
+ decision_end_seq_karar=decision_end_seq_karar,
+ start_date=start_date,
+ end_date=end_date,
+ sort_criteria=sort_criteria,
+ sort_direction=sort_direction,
+ page_number=page_number,
+ page_size=page_size
+ )
+
+ logger.info(f"Tool 'search_emsal_detailed_decisions' called.")
+ try:
+ api_response = await emsal_client_instance.search_detailed_decisions(search_query)
+ if api_response.data:
+ return CompactEmsalSearchResult(
+ decisions=api_response.data.data,
+ 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
+ )
+ 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)
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
+ raise
+
+@app.tool(
+ description="Get Emsal precedent decision text in Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown:
+ """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)
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
+ raise
+
+# --- MCP Tools for Uyusmazlik ---
+@app.tool(
+ description="Search Uyuşmazlık Mahkemesi decisions for jurisdictional disputes",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_uyusmazlik_decisions(
+ icerik: str = Field("", description="Keyword or content for main text search."),
+ bolum: Literal["ALL", "Ceza Bölümü", "Genel Kurul Kararları", "Hukuk Bölümü"] = Field("ALL", description="Select the department (Bölüm). Use 'ALL' for all departments."),
+ uyusmazlik_turu: Literal["ALL", "Görev Uyuşmazlığı", "Hüküm Uyuşmazlığı"] = Field("ALL", description="Select the type of dispute. Use 'ALL' for all types."),
+ karar_sonuclari: List[Literal["Hüküm Uyuşmazlığı Olmadığına Dair", "Hüküm Uyuşmazlığı Olduğuna Dair"]] = Field(default_factory=list, description="List of desired 'Karar Sonucu' types."),
+ esas_yil: str = Field("", description="Case year ('Esas Yılı')."),
+ esas_sayisi: str = Field("", description="Case number ('Esas Sayısı')."),
+ karar_yil: str = Field("", description="Decision year ('Karar Yılı')."),
+ karar_sayisi: str = Field("", description="Decision number ('Karar Sayısı')."),
+ kanun_no: str = Field("", description="Relevant Law Number."),
+ karar_date_begin: str = Field("", description="Decision start date (DD.MM.YYYY)."),
+ karar_date_end: str = Field("", description="Decision end date (DD.MM.YYYY)."),
+ resmi_gazete_sayi: str = Field("", description="Official Gazette number."),
+ resmi_gazete_date: str = Field("", description="Official Gazette date (DD.MM.YYYY)."),
+ tumce: str = Field("", description="Exact phrase search."),
+ wild_card: str = Field("", description="Search for phrase and its inflections."),
+ 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:
+ """Search Court of Jurisdictional Disputes decisions."""
+
+ # Convert string literals to enums
+ # Map "ALL" to TUMU for backward compatibility
+ if bolum == "ALL":
+ bolum_enum = UyusmazlikBolumEnum.TUMU
+ else:
+ bolum_enum = UyusmazlikBolumEnum(bolum) if bolum else UyusmazlikBolumEnum.TUMU
+
+ if uyusmazlik_turu == "ALL":
+ uyusmazlik_turu_enum = UyusmazlikTuruEnum.TUMU
+ else:
+ uyusmazlik_turu_enum = UyusmazlikTuruEnum(uyusmazlik_turu) if uyusmazlik_turu else UyusmazlikTuruEnum.TUMU
+ karar_sonuclari_enums = [UyusmazlikKararSonucuEnum(ks) for ks in karar_sonuclari]
+
+ search_params = UyusmazlikSearchRequest(
+ icerik=icerik,
+ bolum=bolum_enum,
+ uyusmazlik_turu=uyusmazlik_turu_enum,
+ karar_sonuclari=karar_sonuclari_enums,
+ esas_yil=esas_yil,
+ esas_sayisi=esas_sayisi,
+ karar_yil=karar_yil,
+ karar_sayisi=karar_sayisi,
+ kanun_no=kanun_no,
+ karar_date_begin=karar_date_begin,
+ karar_date_end=karar_date_end,
+ resmi_gazete_sayi=resmi_gazete_sayi,
+ resmi_gazete_date=resmi_gazete_date,
+ tumce=tumce,
+ wild_card=wild_card,
+ hepsi=hepsi,
+ herhangi_birisi=herhangi_birisi,
+ not_hepsi=not_hepsi
+ )
+
+ logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
+ try:
+ return await uyusmazlik_client_instance.search_decisions(search_params)
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
+ raise
+
+@app.tool(
+ description="Get Uyuşmazlık Mahkemesi decision text from URL in Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+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:
+ """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))
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
+ raise
+
+# --- DEACTIVATED: MCP Tools for Anayasa Mahkemesi (Individual Tools) ---
+# Use search_anayasa_unified and get_anayasa_document_unified instead
+
+"""
+@app.tool(
+ description="Search Constitutional Court norm control decisions with comprehensive filtering",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+# DEACTIVATED TOOL - Use search_anayasa_unified instead
+# @app.tool(
+# description="DEACTIVATED - Use search_anayasa_unified instead",
+# annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True}
+# )
+# async def search_anayasa_norm_denetimi_decisions(...) -> AnayasaSearchResult:
+# raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.")
+
+# DEACTIVATED TOOL - Use get_anayasa_document_unified instead
+# @app.tool(...)
+# async def get_anayasa_norm_denetimi_document_markdown(...) -> AnayasaDocumentMarkdown:
+# raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.")
+
+# DEACTIVATED TOOL - Use search_anayasa_unified instead
+# @app.tool(...)
+# async def search_anayasa_bireysel_basvuru_report(...) -> AnayasaBireyselReportSearchResult:
+# raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.")
+
+# DEACTIVATED TOOL - Use get_anayasa_document_unified instead
+# @app.tool(...)
+# async def get_anayasa_bireysel_basvuru_document_markdown(...) -> AnayasaBireyselBasvuruDocumentMarkdown:
+# raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.")
+"""
+
+# --- Unified MCP Tools for Anayasa Mahkemesi ---
+@app.tool(
+ description="Unified search for Constitutional Court decisions: both norm control (normkararlarbilgibankasi) and individual applications (kararlarbilgibankasi) in one tool",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_anayasa_unified(
+ decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi (norm control) or bireysel_basvuru (individual applications)"),
+ keywords: List[str] = Field(default_factory=list, description="Keywords to search for (common parameter)"),
+ page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)"),
+ # results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)"),
+
+ # Norm Denetimi specific parameters (ignored for bireysel_basvuru)
+ keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)"),
+ keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)"),
+ decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi"),
+ application_date_start: str = Field("", description="Application start date (norm_denetimi only)"),
+ application_date_end: str = Field("", description="Application end date (norm_denetimi only)"),
+
+ # Bireysel Başvuru specific parameters (ignored for norm_denetimi)
+ decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)"),
+ decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)"),
+ norm_type: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"] = Field("ALL", description="Norm type (bireysel_basvuru only)"),
+ subject_category: str = Field("", description="Subject category (bireysel_basvuru only)")
+) -> str:
+ logger.info(f"Tool 'search_anayasa_unified' called for decision_type: {decision_type}")
+
+ results_per_page = 10 # Default value
+
+ try:
+ request = AnayasaUnifiedSearchRequest(
+ decision_type=decision_type,
+ keywords=keywords,
+ page_to_fetch=page_to_fetch,
+ results_per_page=results_per_page,
+ keywords_all=keywords_all,
+ keywords_any=keywords_any,
+ decision_type_norm=decision_type_norm,
+ application_date_start=application_date_start,
+ application_date_end=application_date_end,
+ decision_start_date=decision_start_date,
+ decision_end_date=decision_end_date,
+ norm_type=norm_type,
+ subject_category=subject_category
+ )
+
+ result = await anayasa_unified_client_instance.search_unified(request)
+ return json.dumps(result.model_dump(), ensure_ascii=False, indent=2)
+
+ except Exception as e:
+ logger.exception(f"Error in tool 'search_anayasa_unified'.")
+ raise
+
+@app.tool(
+ description="Unified document retrieval for Constitutional Court decisions: auto-detects norm control vs individual applications based on URL",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": False,
+ "idempotentHint": True
+ }
+)
+async def get_anayasa_document_unified(
+ document_url: str = Field(..., description="Document URL from search results"),
+ page_number: int = Field(1, ge=1, description="Page number for paginated content (1-indexed)")
+) -> str:
+ logger.info(f"Tool 'get_anayasa_document_unified' called for URL: {document_url}, Page: {page_number}")
+
+ try:
+ result = await anayasa_unified_client_instance.get_document_unified(document_url, page_number)
+ return json.dumps(result.model_dump(mode='json'), ensure_ascii=False, indent=2)
+
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_anayasa_document_unified'.")
+ raise
+
+# --- MCP Tools for KIK (Kamu İhale Kurulu) ---
+@app.tool(
+ description="Search Public Procurement Authority (KİK) decisions for procurement law disputes",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_kik_decisions(
+ karar_tipi: Literal["rbUyusmazlik", "rbDuzenleyici", "rbMahkeme"] = Field("rbUyusmazlik", description="Type of KIK Decision."),
+ karar_no: str = Field("", description="Decision Number (e.g., '2024/UH.II-1766')."),
+ karar_tarihi_baslangic: str = Field("", description="Decision Date Start (DD.MM.YYYY)."),
+ karar_tarihi_bitis: str = Field("", description="Decision Date End (DD.MM.YYYY)."),
+ basvuru_sahibi: str = Field("", description="Applicant."),
+ ihaleyi_yapan_idare: str = Field("", description="Procuring Entity."),
+ basvuru_konusu_ihale: str = Field("", description="Tender subject of the application."),
+ karar_metni: str = Field("", description="Decision text search. Supports: +word, -word, \"exact phrase\", OR/AND"),
+ yil: str = Field("", description="Year of the decision."),
+ resmi_gazete_tarihi: str = Field("", description="Official Gazette Date (DD.MM.YYYY)."),
+ resmi_gazete_sayisi: str = Field("", description="Official Gazette Number."),
+ page: int = Field(1, ge=1, description="Results page number.")
+) -> KikSearchResult:
+ """Search Public Procurement Authority (KIK) decisions."""
+
+ # Convert string literal to enum
+ karar_tipi_enum = KikKararTipi(karar_tipi)
+
+ search_query = KikSearchRequest(
+ karar_tipi=karar_tipi_enum,
+ karar_no=karar_no,
+ karar_tarihi_baslangic=karar_tarihi_baslangic,
+ karar_tarihi_bitis=karar_tarihi_bitis,
+ basvuru_sahibi=basvuru_sahibi,
+ ihaleyi_yapan_idare=ihaleyi_yapan_idare,
+ basvuru_konusu_ihale=basvuru_konusu_ihale,
+ karar_metni=karar_metni,
+ yil=yil,
+ resmi_gazete_tarihi=resmi_gazete_tarihi,
+ resmi_gazete_sayisi=resmi_gazete_sayisi,
+ page=page
+ )
+
+ logger.info(f"Tool 'search_kik_decisions' called.")
+ try:
+ api_response = await kik_client_instance.search_decisions(search_query)
+ page_param_for_log = search_query.page if hasattr(search_query, 'page') else 1
+ if not api_response.decisions and api_response.total_records == 0 and page_param_for_log == 1:
+ logger.warning(f"KIK search returned no decisions for query.")
+ return api_response
+ except Exception as e:
+ logger.exception(f"Error in KIK search tool 'search_kik_decisions'.")
+ current_page_val = search_query.page if hasattr(search_query, 'page') else 1
+ return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val)
+
+@app.tool(
+ description="Get Public Procurement Authority (KİK) decision text in paginated Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def get_kik_document_markdown(
+ karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."),
+ page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.")
+) -> KikDocumentMarkdown:
+ """Get KIK decision as paginated Markdown."""
+ logger.info(f"Tool 'get_kik_document_markdown' called for KIK karar_id: {karar_id}, Markdown Page: {page_number}")
+
+ if not karar_id or not karar_id.strip():
+ logger.error("KIK Document retrieval: karar_id cannot be empty.")
+ return KikDocumentMarkdown(
+ retrieved_with_karar_id=karar_id,
+ error_message="karar_id is required and must be a non-empty string.",
+ current_page=page_number or 1,
+ total_pages=1,
+ is_paginated=False
+ )
+
+ current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
+
+ try:
+ return await kik_client_instance.get_decision_document_as_markdown(
+ karar_id_b64=karar_id,
+ page_number=current_page_to_fetch
+ )
+ except Exception as e:
+ logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}")
+ return KikDocumentMarkdown(
+ retrieved_with_karar_id=karar_id,
+ error_message=f"Tool-level error during KIK document retrieval: {str(e)}",
+ current_page=current_page_to_fetch,
+ total_pages=1,
+ is_paginated=False
+ )
+@app.tool(
+ description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_rekabet_kurumu_decisions(
+ sayfaAdi: str = Field("", description="Search in decision title (Başlık)."),
+ YayinlanmaTarihi: str = Field("", description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY."),
+ PdfText: str = Field(
+ "",
+ description='Search in decision text. Use "\\"kesin cümle\\"" for precise matching.'
+ ),
+ KararTuru: Literal[
+ "ALL",
+ "Birleşme ve Devralma",
+ "Diğer",
+ "Menfi Tespit ve Muafiyet",
+ "Özelleştirme",
+ "Rekabet İhlali"
+ ] = Field("ALL", description="Parameter description"),
+ 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:
+ """Search Competition Authority decisions."""
+
+ karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
+
+ try:
+ if karar_turu_guid_enum is None:
+ logger.warning(f"Invalid user-provided KararTuru: '{KararTuru}'. Defaulting to TUMU (all).")
+ karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU
+ except Exception as e_map:
+ logger.error(f"Error mapping KararTuru '{KararTuru}': {e_map}. Defaulting to TUMU.")
+ karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU
+
+ search_query = RekabetKurumuSearchRequest(
+ sayfaAdi=sayfaAdi,
+ YayinlanmaTarihi=YayinlanmaTarihi,
+ PdfText=PdfText,
+ KararTuruID=karar_turu_guid_enum,
+ KararSayisi=KararSayisi,
+ KararTarihi=KararTarihi,
+ page=page
+ )
+ 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)
+ 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)
+
+@app.tool(
+ description="Get Competition Authority decision text in paginated Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+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:
+ """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)
+ except Exception as e:
+ logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
+ raise
+
+# --- MCP Tools for Bedesten (Unified Search Across All Courts) ---
+@app.tool(
+ description="Search multiple Turkish courts (Yargıtay, Danıştay, Local Courts, Appeals Courts, KYB)",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_bedesten_unified(
+ ctx: Context,
+ phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS:
+• Simple: "mülkiyet hakkı" (finds both words)
+• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)
+• Required term: "+mülkiyet hakkı" (must contain mülkiyet)
+• Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira)
+• Boolean AND: "mülkiyet AND hak" (both terms required)
+• Boolean OR: "mülkiyet OR tapu" (either term acceptable)
+• Boolean NOT: "mülkiyet NOT satış" (contains mülkiyet but not satış)
+NOTE: Wildcards (*,?), regex patterns (/regex/), fuzzy search (~), and proximity search are NOT supported.
+For best results, use exact phrases with quotes for legal terms."""),
+ court_types: List[BedestenCourtTypeEnum] = Field(
+ default=["YARGITAYKARARI", "DANISTAYKARAR"],
+ description="Court types: YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB"
+ ),
+ # pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-10)"),
+ pageNumber: int = Field(1, ge=1, description="Page number"),
+ birimAdi: BirimAdiEnum = Field("ALL", description="""
+ Chamber filter (optional). Abbreviated values with Turkish names:
+ • Yargıtay: H1-H23 (1-23. Hukuk Dairesi), C1-C23 (1-23. Ceza Dairesi), HGK (Hukuk Genel Kurulu), CGK (Ceza Genel Kurulu), BGK (Büyük Genel Kurulu), HBK (Hukuk Daireleri Başkanlar Kurulu), CBK (Ceza Daireleri Başkanlar Kurulu)
+ • Danıştay: D1-D17 (1-17. Daire), DBGK (Büyük Gen.Kur.), IDDK (İdare Dava Daireleri Kurulu), VDDK (Vergi Dava Daireleri Kurulu), IBK (İçtihatları Birleştirme Kurulu), IIK (İdari İşler Kurulu), DBK (Başkanlar Kurulu), AYIM (Askeri Yüksek İdare Mahkemesi), AYIM1-3 (Askeri Yüksek İdare Mahkemesi 1-3. Daire)
+ """),
+ kararTarihiStart: str = Field("", description="Start date (ISO 8601 format)"),
+ kararTarihiEnd: str = Field("", description="End date (ISO 8601 format)")
+) -> dict:
+ """Search Turkish legal databases via unified Bedesten API."""
+
+ # Get Bearer token information for access control and logging
+ try:
+ access_token: AccessToken = get_access_token()
+ user_id = access_token.client_id
+ user_scopes = access_token.scopes
+
+ # Check for required scopes
+ if "yargi.read" not in user_scopes and "yargi.search" not in user_scopes:
+ raise ToolError(f"Insufficient permissions: 'yargi.read' or 'yargi.search' scope required. Current scopes: {user_scopes}")
+
+ logger.info(f"Tool 'search_bedesten_unified' called by user '{user_id}' with scopes {user_scopes}")
+
+ except Exception as e:
+ # Development mode fallback - allow access without strict token validation
+ logger.warning(f"Bearer token validation failed, using development mode: {str(e)}")
+ user_id = "dev-user"
+ user_scopes = ["yargi.read", "yargi.search"]
+
+ pageSize = 10 # Default value
+
+ search_data = BedestenSearchData(
+ pageSize=pageSize,
+ pageNumber=pageNumber,
+ itemTypeList=court_types,
+ phrase=phrase,
+ birimAdi=birimAdi,
+ kararTarihiStart=kararTarihiStart,
+ kararTarihiEnd=kararTarihiEnd
+ )
+
+ search_request = BedestenSearchRequest(data=search_data)
+
+ logger.info(f"User '{user_id}' searching bedesten: phrase='{phrase}', court_types={court_types}, birimAdi='{birimAdi}', page={pageNumber}")
+
+ try:
+ response = await bedesten_client_instance.search_documents(search_request)
+
+ if response.data is None:
+ return {
+ "decisions": [],
+ "total_records": 0,
+ "requested_page": pageNumber,
+ "page_size": pageSize,
+ "searched_courts": court_types,
+ "error": "No data returned from Bedesten API"
+ }
+
+ return {
+ "decisions": [d.model_dump() for d in response.data.emsalKararList],
+ "total_records": response.data.total,
+ "requested_page": pageNumber,
+ "page_size": pageSize,
+ "searched_courts": court_types
+ }
+ except Exception as e:
+ logger.exception("Error in tool 'search_bedesten_unified'")
+ raise
+
+@app.tool(
+ description="Get legal decision document from Bedesten API in Markdown format",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def get_bedesten_document_markdown(
+ documentId: str = Field(..., description="Document ID from Bedesten search results")
+) -> BedestenDocumentMarkdown:
+ """Get legal decision document as Markdown from Bedesten API."""
+ logger.info(f"Tool 'get_bedesten_document_markdown' called for ID: {documentId}")
+
+ if not documentId or not documentId.strip():
+ raise ValueError("Document ID must be a non-empty string.")
+
+ try:
+ return await bedesten_client_instance.get_document_as_markdown(documentId)
+ except Exception as e:
+ logger.exception("Error in tool 'get_kyb_bedesten_document_markdown'")
+ raise
+
+# --- MCP Tools for Sayıştay (Turkish Court of Accounts) ---
+
+# DEACTIVATED TOOL - Use search_sayistay_unified instead
+# @app.tool(
+# description="Search Sayıştay Genel Kurul decisions for audit and accountability regulations",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": True,
+# "idempotentHint": True
+# }
+# )
+# async def search_sayistay_genel_kurul(
+# karar_no: str = Field("", description="Decision number to search for (e.g., '5415')"),
+# karar_ek: str = Field("", description="Decision appendix number (max 99, e.g., '1')"),
+# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
+# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
+# karar_tamami: str = Field("", description="Full text search"),
+# start: int = Field(0, description="Starting record for pagination (0-based)"),
+# length: int = Field(10, description="Number of records per page (1-100)")
+# ) -> GenelKurulSearchResponse:
+# """Search Sayıştay General Assembly decisions."""
+# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
+
+# DEACTIVATED TOOL - Use search_sayistay_unified instead
+# @app.tool(
+# description="Search Sayıştay Temyiz Kurulu decisions with chamber filtering and comprehensive criteria",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": True,
+# "idempotentHint": True
+# }
+# )
+# async def search_sayistay_temyiz_kurulu(
+# ilam_dairesi: DaireEnum = Field("ALL", description="Audit chamber selection"),
+# yili: str = Field("", description="Year (YYYY)"),
+# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
+# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
+# kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
+# ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
+# dosya_no: str = Field("", description="File number for the case"),
+# temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number"),
+# temyiz_karar: str = Field("", description="Appeals decision text"),
+# web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
+# start: int = Field(0, description="Starting record for pagination (0-based)"),
+# length: int = Field(10, description="Number of records per page (1-100)")
+# ) -> TemyizKuruluSearchResponse:
+# """Search Sayıştay Appeals Board decisions."""
+# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
+
+# DEACTIVATED TOOL - Use search_sayistay_unified instead
+# @app.tool(
+# description="Search Sayıştay Daire decisions with chamber filtering and subject categorization",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": True,
+# "idempotentHint": True
+# }
+# )
+# async def search_sayistay_daire(
+# yargilama_dairesi: DaireEnum = Field("ALL", description="Chamber selection"),
+# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
+# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
+# ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
+# kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
+# hesap_yili: str = Field("", description="Fiscal year"),
+# web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
+# web_karar_metni: str = Field("", description="Decision text search"),
+# start: int = Field(0, description="Starting record for pagination (0-based)"),
+# length: int = Field(10, description="Number of records per page (1-100)")
+# ) -> DaireSearchResponse:
+# """Search Sayıştay Chamber decisions."""
+# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
+
+# DEACTIVATED TOOL - Use get_sayistay_document_unified instead
+# @app.tool(
+# description="Get Sayıştay Genel Kurul decision document in Markdown format",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": False,
+# "idempotentHint": True
+# }
+# )
+# async def get_sayistay_genel_kurul_document_markdown(
+# decision_id: str = Field(..., description="Decision ID from search_sayistay_genel_kurul results")
+# ) -> SayistayDocumentMarkdown:
+# """Get Sayıştay General Assembly decision as Markdown."""
+# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
+
+# DEACTIVATED TOOL - Use get_sayistay_document_unified instead
+# @app.tool(
+# description="Get Sayıştay Temyiz Kurulu decision document in Markdown format",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": False,
+# "idempotentHint": True
+# }
+# )
+# async def get_sayistay_temyiz_kurulu_document_markdown(
+# decision_id: str = Field(..., description="Decision ID from search_sayistay_temyiz_kurulu results")
+# ) -> SayistayDocumentMarkdown:
+# """Get Sayıştay Appeals Board decision as Markdown."""
+# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
+
+# DEACTIVATED TOOL - Use get_sayistay_document_unified instead
+# @app.tool(
+# description="Get Sayıştay Daire decision document in Markdown format",
+# annotations={
+# "readOnlyHint": True,
+# "openWorldHint": False,
+# "idempotentHint": True
+# }
+# )
+# async def get_sayistay_daire_document_markdown(
+# decision_id: str = Field(..., description="Decision ID from search_sayistay_daire results")
+# ) -> SayistayDocumentMarkdown:
+# """Get Sayıştay Chamber decision as Markdown."""
+# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
+
+# --- UNIFIED MCP Tools for Sayıştay (Turkish Court of Accounts) ---
+
+@app.tool(
+ description="Search Sayıştay decisions unified across all three decision types (Genel Kurul, Temyiz Kurulu, Daire) with comprehensive filtering",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_sayistay_unified(
+ decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire"),
+
+ # Common pagination parameters
+ start: int = Field(0, ge=0, description="Starting record for pagination (0-based)"),
+ length: int = Field(10, ge=1, le=100, description="Number of records per page (1-100)"),
+
+ # Common search parameters
+ karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY format)"),
+ karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY format)"),
+ kamu_idaresi_turu: Literal["ALL", "Genel Bütçe Kapsamındaki İdareler", "Yüksek Öğretim Kurumları", "Diğer Özel Bütçeli İdareler", "Düzenleyici ve Denetleyici Kurumlar", "Sosyal Güvenlik Kurumları", "Özel İdareler", "Belediyeler ve Bağlı İdareler", "Diğer"] = Field("ALL", description="Public administration type filter"),
+ ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
+ web_karar_konusu: Literal["ALL", "Harcırah Mevzuatı", "İhale Mevzuatı", "İş Mevzuatı", "Personel Mevzuatı", "Sorumluluk ve Yargılama Usulleri", "Vergi Resmi Harç ve Diğer Gelirler", "Çeşitli Konular"] = Field("ALL", description="Decision subject category filter"),
+
+ # Genel Kurul specific parameters (ignored for other types)
+ karar_no: str = Field("", description="Decision number (genel_kurul only)"),
+ karar_ek: str = Field("", description="Decision appendix number (genel_kurul only)"),
+ karar_tamami: str = Field("", description="Full text search (genel_kurul only)"),
+
+ # Temyiz Kurulu specific parameters (ignored for other types)
+ ilam_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Audit chamber selection (temyiz_kurulu only)"),
+ yili: str = Field("", description="Year (YYYY format, temyiz_kurulu only)"),
+ dosya_no: str = Field("", description="File number (temyiz_kurulu only)"),
+ temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number (temyiz_kurulu only)"),
+ temyiz_karar: str = Field("", description="Appeals decision text search (temyiz_kurulu only)"),
+
+ # Daire specific parameters (ignored for other types)
+ 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:
+ """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,
+ start=start,
+ length=length,
+ karar_tarih_baslangic=karar_tarih_baslangic,
+ karar_tarih_bitis=karar_tarih_bitis,
+ kamu_idaresi_turu=kamu_idaresi_turu,
+ ilam_no=ilam_no,
+ web_karar_konusu=web_karar_konusu,
+ karar_no=karar_no,
+ karar_ek=karar_ek,
+ karar_tamami=karar_tamami,
+ ilam_dairesi=ilam_dairesi,
+ yili=yili,
+ dosya_no=dosya_no,
+ temyiz_tutanak_no=temyiz_tutanak_no,
+ temyiz_karar=temyiz_karar,
+ yargilama_dairesi=yargilama_dairesi,
+ hesap_yili=hesap_yili,
+ web_karar_metni=web_karar_metni
+ )
+ return await sayistay_unified_client_instance.search_unified(search_request)
+ except Exception as e:
+ logger.exception("Error in tool 'search_sayistay_unified'")
+ raise
+
+@app.tool(
+ description="Get Sayıştay decision document in Markdown format for any decision type",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": False,
+ "idempotentHint": True
+ }
+)
+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:
+ """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)
+ except Exception as e:
+ logger.exception("Error in tool 'get_sayistay_document_unified'")
+ raise
+
+# --- Application Shutdown Handling ---
+def perform_cleanup():
+ logger.info("MCP Server performing cleanup...")
+ try:
+ loop = asyncio.get_event_loop_policy().get_event_loop()
+ if loop.is_closed():
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ clients_to_close = [
+ globals().get('yargitay_client_instance'),
+ globals().get('danistay_client_instance'),
+ globals().get('emsal_client_instance'),
+ globals().get('uyusmazlik_client_instance'),
+ globals().get('anayasa_norm_client_instance'),
+ globals().get('anayasa_bireysel_client_instance'),
+ globals().get('anayasa_unified_client_instance'),
+ globals().get('kik_client_instance'),
+ globals().get('rekabet_client_instance'),
+ globals().get('bedesten_client_instance'),
+ globals().get('sayistay_client_instance'),
+ globals().get('sayistay_unified_client_instance'),
+ globals().get('kvkk_client_instance'),
+ globals().get('bddk_client_instance')
+ ]
+ async def close_all_clients_async():
+ tasks = []
+ for client_instance in clients_to_close:
+ 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__}")
+ tasks.append(client_instance.close_client_session())
+ if tasks:
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ for i, result in enumerate(results):
+ if isinstance(result, Exception):
+ client_name = "Unknown Client"
+ if i < len(clients_to_close) and clients_to_close[i] is not None:
+ client_name = clients_to_close[i].__class__.__name__
+ logger.error(f"Error closing client {client_name}: {result}")
+ try:
+ if loop.is_running():
+ asyncio.ensure_future(close_all_clients_async(), loop=loop)
+ logger.info("Client cleanup tasks scheduled on running event loop.")
+ else:
+ loop.run_until_complete(close_all_clients_async())
+ logger.info("Client cleanup tasks completed via run_until_complete.")
+ except Exception as e:
+ logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True)
+ logger.info("MCP Server atexit cleanup process finished.")
+
+atexit.register(perform_cleanup)
+
+# --- Health Check Tools ---
+@app.tool(
+ description="Check if Turkish government legal database servers are operational",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+async def check_government_servers_health() -> Dict[str, Any]:
+ """Check health status of Turkish government legal database servers."""
+ logger.info("Health check tool called for government servers")
+
+ health_results = {}
+
+ # Check Yargıtay server
+ try:
+ yargitay_payload = {
+ "data": {
+ "aranan": "karar",
+ "arananKelime": "karar",
+ "pageSize": 10,
+ "pageNumber": 1
+ }
+ }
+
+ async with httpx.AsyncClient(
+ headers={
+ "Accept": "*/*",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "Connection": "keep-alive",
+ "Content-Type": "application/json; charset=UTF-8",
+ "Origin": "https://karararama.yargitay.gov.tr",
+ "Referer": "https://karararama.yargitay.gov.tr/",
+ "Sec-Fetch-Dest": "empty",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Site": "same-origin",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
+ "X-Requested-With": "XMLHttpRequest"
+ },
+ timeout=30.0,
+ verify=False
+ ) as client:
+ response = await client.post(
+ "https://karararama.yargitay.gov.tr/aramalist",
+ json=yargitay_payload
+ )
+
+ if response.status_code == 200:
+ response_data = response.json()
+ records_total = response_data.get("data", {}).get("recordsTotal", 0)
+
+ if records_total > 0:
+ health_results["yargitay"] = {
+ "status": "healthy",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+ else:
+ health_results["yargitay"] = {
+ "status": "unhealthy",
+ "reason": "recordsTotal is 0 or missing",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+ else:
+ health_results["yargitay"] = {
+ "status": "unhealthy",
+ "reason": f"HTTP {response.status_code}",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+
+ except Exception as e:
+ health_results["yargitay"] = {
+ "status": "unhealthy",
+ "reason": f"Connection error: {str(e)}"
+ }
+
+ # Check Bedesten API server
+ try:
+ bedesten_payload = {
+ "data": {
+ "pageSize": 5,
+ "pageNumber": 1,
+ "itemTypeList": ["YARGITAYKARARI"],
+ "phrase": "karar",
+ "sortFields": ["KARAR_TARIHI"],
+ "sortDirection": "desc"
+ },
+ "applicationName": "UyapMevzuat",
+ "paging": True
+ }
+
+ async with httpx.AsyncClient(
+ headers={
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "User-Agent": "Mozilla/5.0 Health Check"
+ },
+ timeout=30.0,
+ verify=False
+ ) as client:
+ response = await client.post(
+ "https://bedesten.adalet.gov.tr/emsal-karar/searchDocuments",
+ json=bedesten_payload
+ )
+
+ if response.status_code == 200:
+ response_data = response.json()
+ logger.debug(f"Bedesten API response: {response_data}")
+ if response_data and isinstance(response_data, dict):
+ data_section = response_data.get("data")
+ if data_section and isinstance(data_section, dict):
+ total_found = data_section.get("total", 0)
+ else:
+ total_found = 0
+ else:
+ total_found = 0
+
+ if total_found > 0:
+ health_results["bedesten"] = {
+ "status": "healthy",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+ else:
+ health_results["bedesten"] = {
+ "status": "unhealthy",
+ "reason": "total is 0 or missing in data field",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+ else:
+ health_results["bedesten"] = {
+ "status": "unhealthy",
+ "reason": f"HTTP {response.status_code}",
+ "response_time_ms": response.elapsed.total_seconds() * 1000
+ }
+
+ except Exception as e:
+ health_results["bedesten"] = {
+ "status": "unhealthy",
+ "reason": f"Connection error: {str(e)}"
+ }
+
+ # Overall health assessment
+ healthy_servers = sum(1 for server in health_results.values() if server["status"] == "healthy")
+ total_servers = len(health_results)
+
+ overall_status = "healthy" if healthy_servers == total_servers else "degraded" if healthy_servers > 0 else "unhealthy"
+
+ return {
+ "overall_status": overall_status,
+ "healthy_servers": healthy_servers,
+ "total_servers": total_servers,
+ "servers": health_results,
+ "check_timestamp": f"{__import__('datetime').datetime.now().isoformat()}"
+ }
+
+# --- MCP Tools for KVKK ---
+@app.tool(
+ description="Search KVKK data protection authority decisions",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+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:
+ """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
+ except Exception as e:
+ logger.exception(f"Error in KVKK search: {e}")
+ # Return empty result on error
+ return KvkkSearchResult(
+ decisions=[],
+ total_results=0,
+ page=page,
+ pageSize=pageSize,
+ query=keywords
+ )
+
+@app.tool(
+ description="Get KVKK decision document in Markdown format with metadata extraction",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": False,
+ "idempotentHint": True
+ }
+)
+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:
+ """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"),
+ title=None,
+ decision_date=None,
+ decision_number=None,
+ subject_summary=None,
+ markdown_chunk=None,
+ current_page=page_number or 1,
+ total_pages=0,
+ is_paginated=False,
+ error_message="Decision URL is required and cannot be empty."
+ )
+
+ try:
+ # Validate URL format
+ if not decision_url.startswith("https://www.kvkk.gov.tr/"):
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=None,
+ decision_date=None,
+ decision_number=None,
+ subject_summary=None,
+ markdown_chunk=None,
+ current_page=page_number or 1,
+ total_pages=0,
+ is_paginated=False,
+ error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/"
+ )
+
+ 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
+
+ except Exception as e:
+ logger.exception(f"Error retrieving KVKK document: {e}")
+ return KvkkDocumentMarkdown(
+ source_url=HttpUrl(decision_url),
+ title=None,
+ decision_date=None,
+ decision_number=None,
+ subject_summary=None,
+ markdown_chunk=None,
+ current_page=page_number or 1,
+ total_pages=0,
+ is_paginated=False,
+ error_message=f"Error retrieving KVKK document: {str(e)}"
+ )
+
+# --- MCP Tools for BDDK (Banking Regulation Authority) ---
+@app.tool(
+ description="Search BDDK banking regulation decisions",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search_bddk_decisions(
+ keywords: str = Field(..., description="Search keywords in Turkish"),
+ page: int = Field(1, ge=1, description="Page number")
+ # pageSize: int = Field(10, ge=1, le=50, description="Results per page")
+) -> dict:
+ """Search BDDK banking regulation and supervision decisions."""
+ logger.info(f"BDDK search tool called with keywords: {keywords}, page: {page}")
+
+ pageSize = 10 # Default value
+
+ try:
+ search_request = BddkSearchRequest(
+ keywords=keywords,
+ page=page,
+ pageSize=pageSize
+ )
+
+ result = await bddk_client_instance.search_decisions(search_request)
+ logger.info(f"BDDK search completed. Found {len(result.decisions)} decisions on page {page}")
+
+ return {
+ "decisions": [
+ {
+ "title": dec.title,
+ "document_id": dec.document_id,
+ "content": dec.content
+ }
+ for dec in result.decisions
+ ],
+ "total_results": result.total_results,
+ "page": result.page,
+ "pageSize": result.pageSize
+ }
+
+ except Exception as e:
+ logger.exception(f"Error searching BDDK decisions: {e}")
+ return {
+ "decisions": [],
+ "total_results": 0,
+ "page": page,
+ "pageSize": pageSize,
+ "error": str(e)
+ }
+
+@app.tool(
+ description="Get BDDK decision document as Markdown",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": False,
+ "idempotentHint": True
+ }
+)
+async def get_bddk_document_markdown(
+ document_id: str = Field(..., description="BDDK document ID (e.g., '310')"),
+ page_number: int = Field(1, ge=1, description="Page number")
+) -> dict:
+ """Retrieve BDDK decision document in Markdown format."""
+ logger.info(f"BDDK document retrieval tool called for ID: {document_id}, page: {page_number}")
+
+ if not document_id or not document_id.strip():
+ return {
+ "document_id": document_id,
+ "markdown_content": "",
+ "page_number": page_number,
+ "total_pages": 0,
+ "error": "Document ID is required"
+ }
+
+ try:
+ result = await bddk_client_instance.get_document_markdown(document_id, page_number)
+ logger.info(f"BDDK document retrieved successfully. Page {result.page_number}/{result.total_pages}")
+
+ return {
+ "document_id": result.document_id,
+ "markdown_content": result.markdown_content,
+ "page_number": result.page_number,
+ "total_pages": result.total_pages
+ }
+
+ except Exception as e:
+ logger.exception(f"Error retrieving BDDK document: {e}")
+ return {
+ "document_id": document_id,
+ "markdown_content": "",
+ "page_number": page_number,
+ "total_pages": 0,
+ "error": str(e)
+ }
+
+# --- ChatGPT Deep Research Compatible Tools ---
+
+def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars: int = 200) -> str:
+ """
+ Extract a preview of document text by skipping headers and showing meaningful content.
+
+ Args:
+ markdown_content: Full document content in markdown format
+ skip_chars: Number of characters to skip from the beginning (default: 100)
+ preview_chars: Number of characters to show in preview (default: 200)
+
+ Returns:
+ Preview text suitable for ChatGPT Deep Research
+ """
+ if not markdown_content:
+ return ""
+
+ # Remove common markdown artifacts and clean up
+ cleaned_content = markdown_content.strip()
+
+ # Skip the first N characters (usually headers, metadata)
+ if len(cleaned_content) > skip_chars:
+ content_start = cleaned_content[skip_chars:]
+ else:
+ content_start = cleaned_content
+
+ # Get the next N characters for preview
+ if len(content_start) > preview_chars:
+ preview = content_start[:preview_chars]
+ else:
+ preview = content_start
+
+ # Clean up the preview - remove incomplete sentences at the end
+ preview = preview.strip()
+
+ # If preview ends mid-sentence, try to end at last complete sentence
+ if preview and not preview.endswith('.'):
+ last_period = preview.rfind('.')
+ if last_period > 50: # Only if there's a reasonable sentence
+ preview = preview[:last_period + 1]
+
+ # Add ellipsis if content was truncated
+ if len(content_start) > preview_chars:
+ preview += "..."
+
+ 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={
+ "readOnlyHint": True,
+ "openWorldHint": True,
+ "idempotentHint": True
+ }
+)
+async def search(
+ query: str = Field(..., description="Turkish search query")
+) -> Dict[str, List[Dict[str, str]]]:
+ """
+ Bedesten API search tool for ChatGPT Deep Research compatibility.
+
+ This tool searches Turkish legal databases via the unified Bedesten API.
+ It supports advanced search operators and covers all major court types.
+
+ USAGE RESTRICTION: Only for ChatGPT Deep Research workflows.
+ For regular legal research, use search_bedesten_unified with specific court types.
+
+ Returns:
+ Object with "results" field containing a list of documents with id, title, text preview, and url
+ as required by ChatGPT Deep Research specification.
+ """
+ logger.info(f"ChatGPT Deep Research search tool called with query: {query}")
+
+ results = []
+
+ try:
+ # Search all court types via unified Bedesten API
+ court_types = [
+ ("YARGITAYKARARI", "Yargıtay", "yargitay_bedesten"),
+ ("DANISTAYKARAR", "Danıştay", "danistay_bedesten"),
+ ("YERELHUKUK", "Yerel Hukuk Mahkemesi", "yerel_hukuk_bedesten"),
+ ("ISTINAFHUKUK", "İstinaf Hukuk Mahkemesi", "istinaf_hukuk_bedesten"),
+ ("KYB", "Kanun Yararına Bozma", "kyb_bedesten")
+ ]
+
+ for item_type, court_name, id_prefix in court_types:
+ try:
+ search_results = await bedesten_client_instance.search_documents(
+ BedestenSearchRequest(
+ data=BedestenSearchData(
+ phrase=query, # Use query as-is to support both regular and exact phrase searches
+ itemTypeList=[item_type],
+ pageSize=10,
+ pageNumber=1
+ )
+ )
+ )
+
+ # Handle potential None data
+ if search_results.data is None:
+ logger.warning(f"No data returned from Bedesten API for {court_name}")
+ continue
+
+ # Add results from this court type (limit to top 5 per court)
+ for decision in search_results.data.emsalKararList[:5]:
+ # For ChatGPT Deep Research, fetch document content for preview
+ try:
+ # Fetch document content for preview
+ doc = await bedesten_client_instance.get_document_as_markdown(decision.documentId)
+
+ # Generate preview text (skip first 100 chars, show next 200)
+ preview_text = get_preview_text(doc.markdown_content, skip_chars=100, preview_chars=200)
+
+ # Build title from metadata
+ title_parts = []
+ if decision.birimAdi:
+ title_parts.append(decision.birimAdi)
+ if decision.esasNo:
+ title_parts.append(f"Esas: {decision.esasNo}")
+ if decision.kararNo:
+ title_parts.append(f"Karar: {decision.kararNo}")
+ if decision.kararTarihiStr:
+ title_parts.append(f"Tarih: {decision.kararTarihiStr}")
+
+ if title_parts:
+ title = " - ".join(title_parts)
+ else:
+ title = f"{court_name} - Document {decision.documentId}"
+
+ # Add to results in OpenAI format
+ results.append({
+ "id": decision.documentId,
+ "title": title,
+ "text": preview_text,
+ "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}"
+ })
+
+ except Exception as e:
+ logger.warning(f"Could not fetch preview for document {decision.documentId}: {e}")
+ # Add minimal result without preview
+ results.append({
+ "id": decision.documentId,
+ "title": f"{court_name} - Document {decision.documentId}",
+ "text": "Document preview not available",
+ "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}"
+ })
+
+ if search_results.data:
+ logger.info(f"Found {len(search_results.data.emsalKararList)} results from {court_name}")
+ else:
+ logger.info(f"Found 0 results from {court_name} (no data returned)")
+
+ except Exception as e:
+ logger.warning(f"Bedesten API search error for {court_name}: {e}")
+
+ # Comment out other API implementations for ChatGPT Deep Research
+ """
+ # Other API implementations disabled for ChatGPT Deep Research
+ # These are available through specific court tools:
+
+ # Yargıtay Official API - use search_yargitay_detailed instead
+ # Danıştay Official API - use search_danistay_by_keyword instead
+ # Constitutional Court - use search_anayasa_norm_denetimi_decisions instead
+ # Competition Authority - use search_rekabet_kurumu_decisions instead
+ # Public Procurement Authority - use search_kik_decisions instead
+ # Court of Accounts - use search_sayistay_* tools instead
+ # UYAP Emsal - use search_emsal_detailed_decisions instead
+ # Jurisdictional Disputes Court - use search_uyusmazlik_decisions instead
+ """
+
+ logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.")
+ return {"results": 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}
+ raise
+
+@app.tool(
+ description="DO NOT USE unless you are ChatGPT Deep Research. Fetch document by ID. See docs for details",
+ annotations={
+ "readOnlyHint": True,
+ "openWorldHint": False, # Retrieves specific documents, not exploring
+ "idempotentHint": True
+ }
+)
+async def fetch(
+ id: str = Field(..., description="Document identifier from search results (numeric only)")
+) -> Dict[str, Any]:
+ """
+ Bedesten API fetch tool for ChatGPT Deep Research compatibility.
+
+ Retrieves the full text content of Turkish legal documents via unified Bedesten API.
+ Converts documents from HTML/PDF to clean Markdown format.
+
+ USAGE RESTRICTION: Only for ChatGPT Deep Research workflows.
+ For regular legal research, use specific court document tools.
+
+ Input Format:
+ - id: Numeric document identifier from search results (e.g., "730113500", "71370900")
+
+ Returns:
+ Single object with numeric id, title, text (full Markdown content), mevzuat.adalet.gov.tr url, and metadata fields
+ as required by ChatGPT Deep Research specification.
+ """
+ logger.info(f"ChatGPT Deep Research fetch tool called for document ID: {id}")
+
+ if not id or not id.strip():
+ raise ValueError("Document ID must be a non-empty string")
+
+ try:
+ # Use the numeric ID directly with Bedesten API
+ doc = await bedesten_client_instance.get_document_as_markdown(id)
+
+ # Try to get additional metadata by searching for this specific document
+ title = f"Turkish Legal Document {id}"
+ try:
+ # Quick search to get metadata for better title
+ search_results = await bedesten_client_instance.search_documents(
+ BedestenSearchRequest(
+ data=BedestenSearchData(
+ phrase=id, # Search by document ID
+ pageSize=1,
+ pageNumber=1
+ )
+ )
+ )
+
+ if search_results.data and search_results.data.emsalKararList:
+ decision = search_results.data.emsalKararList[0]
+ if decision.documentId == id:
+ # Build a proper title from metadata
+ title_parts = []
+ if decision.birimAdi:
+ title_parts.append(decision.birimAdi)
+ if decision.esasNo:
+ title_parts.append(f"Esas: {decision.esasNo}")
+ if decision.kararNo:
+ title_parts.append(f"Karar: {decision.kararNo}")
+ if decision.kararTarihiStr:
+ title_parts.append(f"Tarih: {decision.kararTarihiStr}")
+
+ if title_parts:
+ title = " - ".join(title_parts)
+ else:
+ title = f"Turkish Legal Decision {id}"
+ except Exception as e:
+ logger.warning(f"Could not fetch metadata for document {id}: {e}")
+
+ return {
+ "id": id,
+ "title": title,
+ "text": doc.markdown_content,
+ "url": f"https://mevzuat.adalet.gov.tr/ictihat/{id}",
+ "metadata": {
+ "database": "Turkish Legal Database via Bedesten API",
+ "document_id": id,
+ "source_url": doc.source_url,
+ "mime_type": doc.mime_type,
+ "api_source": "Bedesten Unified API",
+ "chatgpt_deep_research": True
+ }
+ }
+
+ # Comment out other API implementations for ChatGPT Deep Research
+ """
+ # Other API implementations disabled for ChatGPT Deep Research
+ # These are available through specific court document tools:
+
+ elif id.startswith("yargitay_"):
+ # Yargıtay Official API - use get_yargitay_document_markdown instead
+ doc_id = id.replace("yargitay_", "")
+ doc = await yargitay_client_instance.get_decision_document_as_markdown(doc_id)
+
+ elif id.startswith("danistay_"):
+ # Danıştay Official API - use get_danistay_document_markdown instead
+ doc_id = id.replace("danistay_", "")
+ doc = await danistay_client_instance.get_decision_document_as_markdown(doc_id)
+
+ elif id.startswith("anayasa_"):
+ # Constitutional Court - use get_anayasa_norm_denetimi_document_markdown instead
+ doc_id = id.replace("anayasa_", "")
+ doc = await anayasa_norm_client_instance.get_decision_document_as_markdown(...)
+
+ elif id.startswith("rekabet_"):
+ # Competition Authority - use get_rekabet_kurumu_document instead
+ doc_id = id.replace("rekabet_", "")
+ doc = await rekabet_client_instance.get_decision_document(...)
+
+ elif id.startswith("kik_"):
+ # Public Procurement Authority - use get_kik_decision_document_as_markdown instead
+ doc_id = id.replace("kik_", "")
+ doc = await kik_client_instance.get_decision_document_as_markdown(doc_id)
+
+ elif id.startswith("local_"):
+ # This was already using Bedesten API, but deprecated for ChatGPT Deep Research
+ doc_id = id.replace("local_", "")
+ doc = await bedesten_client_instance.get_document_as_markdown(doc_id)
+ """
+
+ except Exception as e:
+ logger.exception(f"Error fetching ChatGPT Deep Research document {id}")
+ raise
+
+# --- Token Metrics Tool Removed for Optimization ---
+
+def ensure_playwright_browsers():
+ """Ensure Playwright browsers are installed for KIK tool functionality."""
+ try:
+ import subprocess
+ import os
+
+ # Check if chromium is already installed
+ chromium_path = os.path.expanduser("~/Library/Caches/ms-playwright/chromium-1179")
+ if os.path.exists(chromium_path):
+ logger.info("Playwright Chromium browser already installed.")
+ return
+
+ logger.info("Installing Playwright Chromium browser for KIK tool...")
+ result = subprocess.run(
+ ["python", "-m", "playwright", "install", "chromium"],
+ capture_output=True,
+ text=True,
+ timeout=300 # 5 minutes timeout
+ )
+
+ if result.returncode == 0:
+ logger.info("Playwright Chromium browser installed successfully.")
+ else:
+ logger.warning(f"Failed to install Playwright browser: {result.stderr}")
+ logger.warning("KIK tool may not work properly without Playwright browsers.")
+
+ except Exception as e:
+ logger.warning(f"Could not auto-install Playwright browsers: {e}")
+ logger.warning("KIK tool may not work properly. Manual installation: 'playwright install chromium'")
+
+def main():
+ logger.info(f"Starting {app.name} server via main() function...")
+ logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
+
+ # Ensure Playwright browsers are installed
+ ensure_playwright_browsers()
+
+ try:
+ app.run()
+ except KeyboardInterrupt:
+ logger.info("Server shut down by user (KeyboardInterrupt).")
+ except Exception as e:
+ logger.exception("Server failed to start or crashed.")
+ finally:
+ logger.info(f"{app.name} server has shut down.")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/nginx.conf b/saidsurucu-yargi-mcp-f5fa007/nginx.conf
new file mode 100644
index 0000000..5cb39b2
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/nginx.conf
@@ -0,0 +1,94 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ upstream yargi_mcp {
+ server yargi-mcp:8000;
+ }
+
+ # Rate limiting
+ limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
+ limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=100r/s;
+
+ server {
+ listen 80;
+ server_name localhost;
+
+ # Redirect HTTP to HTTPS in production
+ # return 301 https://$server_name$request_uri;
+
+ # Security headers
+ add_header X-Content-Type-Options nosniff;
+ add_header X-Frame-Options DENY;
+ add_header X-XSS-Protection "1; mode=block";
+ add_header Referrer-Policy "strict-origin-when-cross-origin";
+
+ # API endpoints
+ location /api/ {
+ limit_req zone=api_limit burst=20 nodelay;
+
+ proxy_pass http://yargi_mcp;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Timeouts
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+ }
+
+ # MCP endpoint (higher rate limit)
+ location /mcp-server/mcp/ {
+ limit_req zone=mcp_limit burst=50 nodelay;
+
+ proxy_pass http://yargi_mcp;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket support
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Longer timeouts for MCP operations
+ proxy_connect_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # Health check (no rate limit)
+ location /health {
+ proxy_pass http://yargi_mcp;
+ proxy_set_header Host $host;
+ }
+
+ # Root and other paths
+ location / {
+ limit_req zone=api_limit burst=10 nodelay;
+
+ proxy_pass http://yargi_mcp;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+ }
+
+ # SSL configuration (uncomment for production)
+ # server {
+ # listen 443 ssl http2;
+ # server_name your-domain.com;
+ #
+ # ssl_certificate /etc/nginx/ssl/cert.pem;
+ # ssl_certificate_key /etc/nginx/ssl/key.pem;
+ # ssl_protocols TLSv1.2 TLSv1.3;
+ # ssl_ciphers HIGH:!aNULL:!MD5;
+ #
+ # # Include all location blocks from above
+ # }
+}
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/ornek.png b/saidsurucu-yargi-mcp-f5fa007/ornek.png
new file mode 100644
index 0000000..012d2ef
Binary files /dev/null and b/saidsurucu-yargi-mcp-f5fa007/ornek.png differ
diff --git a/saidsurucu-yargi-mcp-f5fa007/pyproject.toml b/saidsurucu-yargi-mcp-f5fa007/pyproject.toml
new file mode 100644
index 0000000..e90bea6
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/pyproject.toml
@@ -0,0 +1,66 @@
+[project]
+name = "yargi-mcp"
+version = "0.1.6"
+description = "MCP Server For Turkish Legal Databases"
+readme = "README.md"
+requires-python = ">=3.11"
+license = {text = "MIT"}
+authors = [{name = "Said Surucu", email = "saidsrc@gmail.com"}]
+keywords = ["mcp", "turkish-law", "legal", "yargitay", "danistay", "bddk", "kvkk", "turkish", "law", "court", "decisions"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Legal Industry",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Text Processing :: Markup :: Markdown",
+ "Operating System :: OS Independent",
+]
+urls = {Homepage = "https://github.com/saidsurucu/yargi-mcp", Issues = "https://github.com/saidsurucu/yargi-mcp/issues"}
+dependencies = [
+ "beautifulsoup4>=4.13.4",
+ "httpx>=0.28.1",
+ "markitdown[pdf]>=0.1.1",
+ "pydantic>=2.11.4",
+ "aiohttp>=3.11.18",
+ "playwright>=1.52.0",
+ "fastmcp>=2.10.5",
+ "pypdf>=5.5.0",
+ "fastapi>=0.115.14",
+ "PyJWT>=2.8.0",
+ "tiktoken>=0.5.0",
+]
+
+[project.optional-dependencies]
+asgi = [
+ "uvicorn[standard]>=0.30.0",
+ "starlette>=0.37.0",
+]
+api = [
+ "fastapi>=0.115.0",
+ "uvicorn[standard]>=0.30.0",
+]
+production = [
+ "gunicorn>=22.0.0",
+ "uvicorn[standard]>=0.30.0",
+]
+saas = [
+ "clerk-backend-api>=3.0.0",
+ "stripe>=9.1.0",
+ "upstash-redis>=1.1.0",
+]
+
+[project.scripts]
+yargi-mcp = "mcp_server_main:main"
+
+[tool.setuptools]
+py-modules = ["mcp_server_main", "mcp_auth_factory", "mcp_auth_http_adapter", "asgi_app", "fastapi_app", "starlette_app", "run_asgi", "stripe_webhook"]
+
+[tool.setuptools.packages.find]
+include = ["*_mcp_module", "mcp_auth"]
+
+[build-system]
+requires = ["setuptools>=65.0", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/saidsurucu-yargi-mcp-f5fa007/railway.json b/saidsurucu-yargi-mcp-f5fa007/railway.json
new file mode 100644
index 0000000..0c54927
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/railway.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://railway.app/railway.schema.json",
+ "build": {
+ "builder": "NIXPACKS",
+ "buildCommand": "pip install -e .[asgi]"
+ },
+ "deploy": {
+ "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT",
+ "healthcheckPath": "/health",
+ "healthcheckTimeout": 30,
+ "restartPolicyType": "ON_FAILURE",
+ "restartPolicyMaxRetries": 3
+ },
+ "variables": {
+ "ALLOWED_ORIGINS": "*",
+ "LOG_LEVEL": "info"
+ }
+}
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py b/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py
new file mode 100644
index 0000000..0eb1142
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py
@@ -0,0 +1,464 @@
+"""
+Redis Session Store for OAuth Authorization Codes and User Sessions
+
+This module provides Redis-based storage for OAuth authorization codes and user sessions,
+enabling multi-machine deployment support by replacing in-memory storage.
+
+Uses Upstash Redis via REST API for serverless-friendly operation.
+"""
+
+import os
+import json
+import time
+import logging
+from typing import Optional, Dict, Any, Union
+from datetime import datetime, timedelta
+
+logger = logging.getLogger(__name__)
+
+try:
+ from upstash_redis import Redis
+ UPSTASH_AVAILABLE = True
+except ImportError:
+ UPSTASH_AVAILABLE = False
+ Redis = None
+
+# Use standard Python exceptions for Redis connection errors
+import socket
+from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout as RequestsTimeout
+
+class RedisSessionStore:
+ """
+ Redis-based session store for OAuth flows and user sessions.
+
+ Uses Upstash Redis REST API for connection-free operation suitable for
+ multi-instance deployments on platforms like Fly.io.
+ """
+
+ def __init__(self):
+ """Initialize Redis connection using environment variables."""
+ if not UPSTASH_AVAILABLE:
+ raise ImportError("upstash-redis package is required. Install with: pip install upstash-redis")
+
+ # Initialize Upstash Redis client from environment with optimized connection settings
+ try:
+ # Get Upstash Redis configuration
+ redis_url = os.getenv("UPSTASH_REDIS_REST_URL")
+ redis_token = os.getenv("UPSTASH_REDIS_REST_TOKEN")
+
+ if not redis_url or not redis_token:
+ raise ValueError("UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be set")
+
+ logger.info(f"Connecting to Upstash Redis at {redis_url[:30]}...")
+
+ # Initialize with explicit configuration for better SSL handling
+ self.redis = Redis(
+ url=redis_url,
+ token=redis_token
+ )
+
+ logger.info("Upstash Redis client created")
+
+ # Skip connection test during initialization to prevent server hang
+ # Connection will be tested during first actual operation
+ logger.info("Redis client initialized - connection will be tested on first use")
+
+ except Exception as e:
+ logger.error(f"Failed to initialize Upstash Redis: {e}")
+ raise
+
+ # TTL values (in seconds)
+ self.oauth_code_ttl = int(os.getenv("OAUTH_CODE_TTL", "600")) # 10 minutes
+ self.session_ttl = int(os.getenv("SESSION_TTL", "3600")) # 1 hour
+
+ def _serialize_data(self, data: Dict[str, Any]) -> Dict[str, str]:
+ """Convert data to Redis-compatible string format."""
+ serialized = {}
+ for key, value in data.items():
+ if isinstance(value, (dict, list)):
+ serialized[key] = json.dumps(value)
+ elif isinstance(value, (int, float)):
+ serialized[key] = str(value)
+ elif isinstance(value, bool):
+ serialized[key] = "true" if value else "false"
+ else:
+ serialized[key] = str(value)
+ return serialized
+
+ def _deserialize_data(self, data: Dict[str, str]) -> Dict[str, Any]:
+ """Convert Redis string data back to original types."""
+ if not data:
+ return {}
+
+ deserialized = {}
+ for key, value in data.items():
+ if not isinstance(value, str):
+ deserialized[key] = value
+ continue
+
+ # Try to deserialize JSON
+ if value.startswith(('[', '{')):
+ try:
+ deserialized[key] = json.loads(value)
+ continue
+ except json.JSONDecodeError:
+ pass
+
+ # Try to convert numbers
+ if value.isdigit():
+ deserialized[key] = int(value)
+ continue
+
+ if value.replace('.', '').isdigit():
+ try:
+ deserialized[key] = float(value)
+ continue
+ except ValueError:
+ pass
+
+ # Handle booleans
+ if value in ("true", "false"):
+ deserialized[key] = value == "true"
+ continue
+
+ # Keep as string
+ deserialized[key] = value
+
+ return deserialized
+
+ # OAuth Authorization Code Methods
+
+ def set_oauth_code(self, code: str, data: Dict[str, Any]) -> bool:
+ """
+ Store OAuth authorization code with automatic expiration.
+
+ Args:
+ code: Authorization code string
+ data: Code data including user_id, client_id, etc.
+
+ Returns:
+ True if stored successfully, False otherwise
+ """
+ try:
+ key = f"oauth:code:{code}"
+
+ # Add timestamp for debugging
+ data_with_timestamp = data.copy()
+ data_with_timestamp.update({
+ "created_at": time.time(),
+ "expires_at": time.time() + self.oauth_code_ttl
+ })
+
+ # Serialize and store - Upstash Redis doesn't support mapping parameter
+ serialized_data = self._serialize_data(data_with_timestamp)
+
+ # Use individual hset calls for each field with retry logic
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ # Clear any existing data first
+ self.redis.delete(key)
+
+ # Set all fields in a pipeline-like manner
+ for field, value in serialized_data.items():
+ self.redis.hset(key, field, value)
+
+ # Set expiration
+ self.redis.expire(key, self.oauth_code_ttl)
+
+ logger.info(f"Stored OAuth code {code[:10]}... with TTL {self.oauth_code_ttl}s (attempt {attempt + 1})")
+ return True
+
+ except (RequestsConnectionError, RequestsTimeout, OSError, socket.error) as e:
+ logger.warning(f"Redis connection error on attempt {attempt + 1}: {e}")
+ if attempt == max_retries - 1:
+ raise # Re-raise on final attempt
+ time.sleep(0.5 * (attempt + 1)) # Exponential backoff
+
+ except Exception as e:
+ logger.error(f"Failed to store OAuth code {code[:10]}... after {max_retries} attempts: {e}")
+ return False
+
+ def get_oauth_code(self, code: str, delete_after_use: bool = True) -> Optional[Dict[str, Any]]:
+ """
+ Retrieve OAuth authorization code data.
+
+ Args:
+ code: Authorization code string
+ delete_after_use: If True, delete the code after retrieval (one-time use)
+
+ Returns:
+ Code data dictionary or None if not found/expired
+ """
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ key = f"oauth:code:{code}"
+
+ # Get all hash fields with retry
+ data = self.redis.hgetall(key)
+
+ if not data:
+ logger.warning(f"OAuth code {code[:10]}... not found or expired (attempt {attempt + 1})")
+ return None
+
+ # Deserialize data
+ deserialized_data = self._deserialize_data(data)
+
+ # Check manual expiration (in case Redis TTL failed)
+ expires_at = deserialized_data.get("expires_at", 0)
+ if expires_at and time.time() > expires_at:
+ logger.warning(f"OAuth code {code[:10]}... manually expired")
+ try:
+ self.redis.delete(key)
+ except Exception as del_error:
+ logger.warning(f"Failed to delete expired code: {del_error}")
+ return None
+
+ # Delete after use for security (one-time use)
+ if delete_after_use:
+ try:
+ self.redis.delete(key)
+ logger.info(f"Retrieved and deleted OAuth code {code[:10]}... (attempt {attempt + 1})")
+ except Exception as del_error:
+ logger.warning(f"Failed to delete code after use: {del_error}")
+ # Continue anyway since we got the data
+ else:
+ logger.info(f"Retrieved OAuth code {code[:10]}... (not deleted, attempt {attempt + 1})")
+
+ return deserialized_data
+
+ except (RequestsConnectionError, RequestsTimeout, OSError, socket.error) as e:
+ logger.warning(f"Redis connection error on retrieval attempt {attempt + 1}: {e}")
+ if attempt == max_retries - 1:
+ logger.error(f"Failed to retrieve OAuth code {code[:10]}... after {max_retries} attempts: {e}")
+ return None
+ time.sleep(0.5 * (attempt + 1)) # Exponential backoff
+
+ except Exception as e:
+ logger.error(f"Failed to retrieve OAuth code {code[:10]}... on attempt {attempt + 1}: {e}")
+ if attempt == max_retries - 1:
+ return None
+ time.sleep(0.5 * (attempt + 1))
+
+ return None
+
+ # User Session Methods
+
+ def set_session(self, session_id: str, user_data: Dict[str, Any]) -> bool:
+ """
+ Store user session data with sliding expiration.
+
+ Args:
+ session_id: Unique session identifier
+ user_data: User session data (user_id, email, scopes, etc.)
+
+ Returns:
+ True if stored successfully, False otherwise
+ """
+ try:
+ key = f"session:{session_id}"
+
+ # Add session metadata
+ session_data = user_data.copy()
+ session_data.update({
+ "session_id": session_id,
+ "created_at": time.time(),
+ "last_accessed": time.time()
+ })
+
+ # Serialize and store - Upstash Redis doesn't support mapping parameter
+ serialized_data = self._serialize_data(session_data)
+
+ # Use individual hset calls for each field (Upstash compatibility)
+ for field, value in serialized_data.items():
+ self.redis.hset(key, field, value)
+ self.redis.expire(key, self.session_ttl)
+
+ logger.info(f"Stored session {session_id[:10]}... with TTL {self.session_ttl}s")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to store session {session_id[:10]}...: {e}")
+ return False
+
+ def get_session(self, session_id: str, refresh_ttl: bool = True) -> Optional[Dict[str, Any]]:
+ """
+ Retrieve user session data.
+
+ Args:
+ session_id: Session identifier
+ refresh_ttl: If True, extend session TTL on access
+
+ Returns:
+ Session data dictionary or None if not found/expired
+ """
+ try:
+ key = f"session:{session_id}"
+
+ # Get session data
+ data = self.redis.hgetall(key)
+
+ if not data:
+ logger.warning(f"Session {session_id[:10]}... not found or expired")
+ return None
+
+ # Deserialize data
+ session_data = self._deserialize_data(data)
+
+ # Update last accessed time and refresh TTL
+ if refresh_ttl:
+ session_data["last_accessed"] = time.time()
+ self.redis.hset(key, "last_accessed", str(time.time()))
+ self.redis.expire(key, self.session_ttl)
+ logger.debug(f"Refreshed session {session_id[:10]}... TTL")
+
+ return session_data
+
+ except Exception as e:
+ logger.error(f"Failed to retrieve session {session_id[:10]}...: {e}")
+ return None
+
+ def delete_session(self, session_id: str) -> bool:
+ """
+ Delete user session (logout).
+
+ Args:
+ session_id: Session identifier
+
+ Returns:
+ True if deleted successfully, False otherwise
+ """
+ try:
+ key = f"session:{session_id}"
+ result = self.redis.delete(key)
+
+ if result:
+ logger.info(f"Deleted session {session_id[:10]}...")
+ return True
+ else:
+ logger.warning(f"Session {session_id[:10]}... not found for deletion")
+ return False
+
+ except Exception as e:
+ logger.error(f"Failed to delete session {session_id[:10]}...: {e}")
+ return False
+
+ # Health Check Methods
+
+ def health_check(self) -> Dict[str, Any]:
+ """
+ Perform Redis health check.
+
+ Returns:
+ Health status dictionary
+ """
+ try:
+ # Test basic operations
+ test_key = f"health:check:{int(time.time())}"
+ test_value = {"timestamp": time.time(), "test": True}
+
+ # Test set - Use individual hset calls for Upstash compatibility
+ serialized_test = self._serialize_data(test_value)
+ for field, value in serialized_test.items():
+ self.redis.hset(test_key, field, value)
+
+ # Test get
+ retrieved = self.redis.hgetall(test_key)
+
+ # Test delete
+ self.redis.delete(test_key)
+
+ return {
+ "status": "healthy",
+ "redis_connected": True,
+ "operations_working": bool(retrieved),
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+ except Exception as e:
+ logger.error(f"Redis health check failed: {e}")
+ return {
+ "status": "unhealthy",
+ "redis_connected": False,
+ "error": str(e),
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+ def get_stats(self) -> Dict[str, Any]:
+ """
+ Get Redis usage statistics.
+
+ Returns:
+ Statistics dictionary
+ """
+ try:
+ # Get basic info (not all Upstash plans support INFO command)
+ stats = {
+ "oauth_codes_pattern": "oauth:code:*",
+ "sessions_pattern": "session:*",
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+ try:
+ # Try to get counts (may fail on some Upstash plans)
+ oauth_keys = self.redis.keys("oauth:code:*")
+ session_keys = self.redis.keys("session:*")
+
+ stats.update({
+ "active_oauth_codes": len(oauth_keys) if oauth_keys else 0,
+ "active_sessions": len(session_keys) if session_keys else 0
+ })
+ except Exception as e:
+ logger.warning(f"Could not get detailed stats: {e}")
+ stats["warning"] = "Detailed stats not available on this Redis plan"
+
+ return stats
+
+ except Exception as e:
+ logger.error(f"Failed to get Redis stats: {e}")
+ return {"error": str(e), "timestamp": datetime.utcnow().isoformat()}
+
+# Global instance for easy importing
+redis_store = None
+
+def get_redis_store() -> Optional[RedisSessionStore]:
+ """
+ Get global Redis store instance (singleton pattern).
+
+ Returns:
+ RedisSessionStore instance or None if initialization fails
+ """
+ global redis_store
+
+ if redis_store is None:
+ try:
+ logger.info("Initializing Redis store...")
+ redis_store = RedisSessionStore()
+ logger.info("Redis store initialized successfully")
+ except Exception as e:
+ logger.error(f"Failed to initialize Redis store: {e}")
+ redis_store = None
+
+ return redis_store
+
+def init_redis_store() -> RedisSessionStore:
+ """
+ Initialize Redis store and perform health check.
+
+ Returns:
+ RedisSessionStore instance
+
+ Raises:
+ Exception if Redis is not available or unhealthy
+ """
+ store = get_redis_store()
+
+ # Perform health check
+ health = store.health_check()
+
+ if health["status"] != "healthy":
+ raise Exception(f"Redis health check failed: {health}")
+
+ logger.info("Redis session store initialized and healthy")
+ return store
\ No newline at end of file
diff --git a/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py
new file mode 100644
index 0000000..1b6e782
--- /dev/null
+++ b/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py
@@ -0,0 +1,407 @@
+# rekabet_mcp_module/client.py
+
+import httpx
+from bs4 import BeautifulSoup
+from typing import List, Optional, Tuple, Dict, Any
+import logging
+import html
+import re
+import io # For io.BytesIO
+from urllib.parse import urlencode, urljoin, quote, parse_qs, urlparse
+from markitdown import MarkItDown
+import math
+
+# pypdf for PDF processing (lighter alternative to PyMuPDF)
+from pypdf import PdfReader, PdfWriter # PyPDF2'nin devamı niteliğindeki pypdf
+
+from .models import (
+ RekabetKurumuSearchRequest,
+ RekabetDecisionSummary,
+ RekabetSearchResult,
+ RekabetDocument,
+ RekabetKararTuruGuidEnum
+)
+from pydantic import HttpUrl # Ensure HttpUrl is imported from pydantic
+
+logger = logging.getLogger(__name__)
+if not logger.hasHandlers(): # Pragma: no cover
+ logging.basicConfig(
+ level=logging.INFO, # Varsayılan log seviyesi
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+ # Debug betiğinde daha detaylı loglama için seviye ayrıca ayarlanabilir.
+
+class RekabetKurumuApiClient:
+ BASE_URL = "https://www.rekabet.gov.tr"
+ SEARCH_PATH = "/tr/Kararlar"
+ DECISION_LANDING_PATH_TEMPLATE = "/Karar"
+ # PDF sayfa bazlı Markdown döndürüldüğü için bu sabit artık doğrudan kullanılmıyor.
+ # DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
+
+ def __init__(self, request_timeout: float = 60.0):
+ self.http_client = httpx.AsyncClient(
+ base_url=self.BASE_URL,
+ headers={
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+ },
+ timeout=request_timeout,
+ verify=True,
+ follow_redirects=True
+ )
+
+ def _build_search_query_params(self, params: RekabetKurumuSearchRequest) -> List[Tuple[str, str]]:
+ query_params: List[Tuple[str, str]] = []
+ query_params.append(("sayfaAdi", params.sayfaAdi if params.sayfaAdi is not None else ""))
+ query_params.append(("YayinlanmaTarihi", params.YayinlanmaTarihi if params.YayinlanmaTarihi is not None else ""))
+ query_params.append(("PdfText", params.PdfText if params.PdfText is not None else ""))
+
+ karar_turu_id_value = ""
+ if params.KararTuruID is not None:
+ karar_turu_id_value = params.KararTuruID.value if params.KararTuruID.value != "ALL" else ""
+ query_params.append(("KararTuruID", karar_turu_id_value))
+
+ query_params.append(("KararSayisi", params.KararSayisi if params.KararSayisi is not None else ""))
+ query_params.append(("KararTarihi", params.KararTarihi if params.KararTarihi is not None else ""))
+
+ if params.page and params.page > 1:
+ query_params.append(("page", str(params.page)))
+
+ return query_params
+
+ async def search_decisions(self, params: RekabetKurumuSearchRequest) -> RekabetSearchResult:
+ request_path = self.SEARCH_PATH
+ final_query_params = self._build_search_query_params(params)
+ logger.info(f"RekabetKurumuApiClient: Performing search. Path: {request_path}, Parameters: {final_query_params}")
+
+ try:
+ response = await self.http_client.get(request_path, params=final_query_params)
+ response.raise_for_status()
+ html_content = response.text
+ except httpx.RequestError as e:
+ logger.error(f"RekabetKurumuApiClient: HTTP request error during search: {e}")
+ raise
+
+ soup = BeautifulSoup(html_content, 'html.parser')
+ processed_decisions: List[RekabetDecisionSummary] = []
+ total_records: Optional[int] = None
+ total_pages: Optional[int] = None
+
+ pagination_div = soup.find("div", class_="yazi01")
+ if pagination_div:
+ text_content = pagination_div.get_text(separator=" ", strip=True)
+ total_match = re.search(r"Toplam\s*:\s*(\d+)", text_content)
+ if total_match:
+ try:
+ total_records = int(total_match.group(1))
+ logger.debug(f"Total records found from pagination: {total_records}")
+ except ValueError:
+ logger.warning(f"Could not convert 'Toplam' value to int: {total_match.group(1)}")
+ else:
+ logger.warning("'Toplam :' string not found in pagination section.")
+
+ results_per_page_assumed = 10
+ if total_records is not None:
+ calculated_total_pages = math.ceil(total_records / results_per_page_assumed)
+ total_pages = calculated_total_pages if calculated_total_pages > 0 else (1 if total_records > 0 else 0)
+ logger.debug(f"Calculated total pages: {total_pages}")
+
+ if total_pages is None: # Fallback if total_records couldn't be parsed
+ last_page_link = pagination_div.select_one("li.PagedList-skipToLast a")
+ if last_page_link and last_page_link.has_attr('href'):
+ qs = parse_qs(urlparse(last_page_link['href']).query)
+ if 'page' in qs and qs['page']:
+ try:
+ total_pages = int(qs['page'][0])
+ logger.debug(f"Total pages found from 'Last >>' link: {total_pages}")
+ except ValueError:
+ logger.warning(f"Could not convert page value from 'Last >>' link to int: {qs['page'][0]}")
+ elif total_records == 0 : total_pages = 0 # If no records, 0 pages
+ elif total_records is not None and total_records > 0 : total_pages = 1 # If records exist but no last page link (e.g. single page)
+ else: logger.warning("'Last >>' link not found in pagination section.")
+
+ decision_tables_container = soup.find("div", id="kararList")
+ if not decision_tables_container:
+ logger.warning("`div#kararList` (decision list container) not found. HTML structure might have changed or no decisions on this page.")
+ else:
+ decision_tables = decision_tables_container.find_all("table", class_="equalDivide")
+ logger.info(f"Found {len(decision_tables)} 'table' elements with class='equalDivide' for parsing.")
+
+ if not decision_tables and total_records is not None and total_records > 0 :
+ logger.warning(f"Page indicates {total_records} records but no decision tables found with class='equalDivide'.")
+
+ for idx, table in enumerate(decision_tables):
+ logger.debug(f"Processing table {idx + 1}...")
+ try:
+ rows = table.find_all("tr")
+ if len(rows) != 3:
+ logger.warning(f"Table {idx + 1} has an unexpected number of rows ({len(rows)} instead of 3). Skipping. HTML snippet:\n{table.prettify()[:500]}")
+ continue
+
+ # Row 1: Publication Date, Decision Number, Related Cases Link
+ td_elements_r1 = rows[0].find_all("td")
+ pub_date = td_elements_r1[0].get_text(strip=True) if len(td_elements_r1) > 0 else None
+ dec_num = td_elements_r1[1].get_text(strip=True) if len(td_elements_r1) > 1 else None
+
+ related_cases_link_tag = td_elements_r1[2].find("a", href=True) if len(td_elements_r1) > 2 else None
+ related_cases_url_str: Optional[str] = None
+ karar_id_from_related: Optional[str] = None
+ if related_cases_link_tag and related_cases_link_tag.has_attr('href'):
+ related_cases_url_str = urljoin(self.BASE_URL, related_cases_link_tag['href'])
+ qs_related = parse_qs(urlparse(related_cases_link_tag['href']).query)
+ if 'kararId' in qs_related and qs_related['kararId']:
+ karar_id_from_related = qs_related['kararId'][0]
+
+ # Row 2: Decision Date, Decision Type
+ td_elements_r2 = rows[1].find_all("td")
+ dec_date = td_elements_r2[0].get_text(strip=True) if len(td_elements_r2) > 0 else None
+ dec_type_text = td_elements_r2[1].get_text(strip=True) if len(td_elements_r2) > 1 else None
+
+ # Row 3: Title and Main Decision Link
+ title_cell = rows[2].find("td", colspan="5")
+ decision_link_tag = title_cell.find("a", href=True) if title_cell else None
+
+ title_text: Optional[str] = None
+ decision_landing_url_str: Optional[str] = None
+ karar_id_from_main_link: Optional[str] = None
+
+ if decision_link_tag and decision_link_tag.has_attr('href'):
+ title_text = decision_link_tag.get_text(strip=True)
+ href_val = decision_link_tag['href']
+ if href_val.startswith(self.DECISION_LANDING_PATH_TEMPLATE + "?kararId="): # Ensure it's a decision link
+ decision_landing_url_str = urljoin(self.BASE_URL, href_val)
+ qs_main = parse_qs(urlparse(href_val).query)
+ if 'kararId' in qs_main and qs_main['kararId']:
+ karar_id_from_main_link = qs_main['kararId'][0]
+ else:
+ logger.warning(f"Table {idx+1} decision link has unexpected format: {href_val}")
+ else:
+ logger.warning(f"Table {idx+1} could not find title/decision link tag.")
+
+ current_karar_id = karar_id_from_main_link or karar_id_from_related
+
+ if not current_karar_id:
+ logger.warning(f"Table {idx+1} Karar ID not found. Skipping. Title (if any): {title_text}")
+ continue
+
+ # Convert string URLs to HttpUrl for the model
+ final_decision_url = HttpUrl(decision_landing_url_str) if decision_landing_url_str else None
+ final_related_cases_url = HttpUrl(related_cases_url_str) if related_cases_url_str else None
+
+ processed_decisions.append(RekabetDecisionSummary(
+ publication_date=pub_date, decision_number=dec_num, decision_date=dec_date,
+ decision_type_text=dec_type_text, title=title_text,
+ decision_url=final_decision_url,
+ karar_id=current_karar_id,
+ related_cases_url=final_related_cases_url
+ ))
+ logger.debug(f"Table {idx+1} parsed successfully: Karar ID '{current_karar_id}', Title '{title_text[:50] if title_text else 'N/A'}...'")
+
+ except Exception as e:
+ logger.warning(f"RekabetKurumuApiClient: Error parsing decision summary {idx+1}: {e}. Problematic Table HTML:\n{table.prettify()}", exc_info=True)
+ continue
+
+ return RekabetSearchResult(
+ decisions=processed_decisions, total_records_found=total_records,
+ retrieved_page_number=params.page, total_pages=total_pages if total_pages is not None else 0
+ )
+
+ async def _extract_pdf_url_and_landing_page_metadata(self, karar_id: str, landing_page_html: str, landing_page_url: str) -> Dict[str, Any]:
+ soup = BeautifulSoup(landing_page_html, 'html.parser')
+ data: Dict[str, Any] = {
+ "pdf_url": None,
+ "title_on_landing_page": soup.title.string.strip() if soup.title and soup.title.string else f"Rekabet Kurumu Kararı {karar_id}",
+ }
+ # This part needs to be robust and specific to Rekabet Kurumu's landing page structure.
+ # Look for common patterns: direct links, download buttons, embedded viewers.
+ pdf_anchor = soup.find("a", href=re.compile(r"\.pdf(\?|$)", re.IGNORECASE)) # Basic PDF link
+ if not pdf_anchor: # Try other common patterns if the basic one fails
+ # Example: Look for links with specific text or class
+ pdf_anchor = soup.find("a", string=re.compile(r"karar metni|pdf indir", re.IGNORECASE))
+
+ if pdf_anchor and pdf_anchor.has_attr('href'):
+ pdf_path = pdf_anchor['href']
+ data["pdf_url"] = urljoin(landing_page_url, pdf_path)
+ logger.info(f"PDF link found on landing page (): {data['pdf_url']}")
+ else:
+ iframe_pdf = soup.find("iframe", src=re.compile(r"\.pdf(\?|$)", re.IGNORECASE))
+ if iframe_pdf and iframe_pdf.has_attr('src'):
+ pdf_path = iframe_pdf['src']
+ data["pdf_url"] = urljoin(landing_page_url, pdf_path)
+ logger.info(f"PDF link found on landing page (