diff --git a/.serena/memories/anayasa_mahkemesi_architecture.md b/.serena/memories/anayasa_mahkemesi_architecture.md
new file mode 100644
index 0000000..ccc0cda
--- /dev/null
+++ b/.serena/memories/anayasa_mahkemesi_architecture.md
@@ -0,0 +1,278 @@
+# Constitutional Court (Anayasa Mahkemesi) Implementation - Architecture Analysis
+
+## Overview
+The Anayasa Mahkemesi module provides comprehensive access to Turkish Constitutional Court decisions through two separate systems:
+1. **Norm Denetimi** (Norm Control) - Judicial review of laws
+2. **Bireysel Başvuru** (Individual Applications) - Individual constitutional complaints
+
+Both systems have been **unified** into a single MCP interface (Phase 6 optimization - 361 tokens saved).
+
+## Current Architecture
+
+### 1. Module Structure
+```
+anayasa_mcp_module/
+├── __init__.py # Empty
+├── models.py # Pydantic data models (230 lines)
+├── client.py # Norm Denetimi client (356 lines)
+├── bireysel_client.py # Bireysel Başvuru client (355 lines)
+└── unified_client.py # Unified routing logic (122 lines)
+```
+
+### 2. API Endpoints
+
+**Norm Denetimi API:**
+- Base: `https://normkararlarbilgibankasi.anayasa.gov.tr`
+- Search: GET `/Ara` (with query parameters)
+- Document: Dynamic URLs from search results
+
+**Bireysel Başvuru API:**
+- Base: `https://kararlarbilgibankasi.anayasa.gov.tr`
+- Search: GET `/Ara?KararBulteni=1` (with query parameters for report-style results)
+- Document: Dynamic paths like `/BB/YYYY/NNNN`
+
+### 3. Current Search Implementation (Keyword-Based)
+
+**Norm Denetimi Search Parameters (19 parameters):**
+- Keyword logic: `keywords_all[]`, `keywords_any[]`, `keywords_exclude[]` (AND/OR/NOT)
+- Identifiers: case_number_esas, decision_number_karar
+- Dates: first_review_date_start/end, decision_date_start/end, official_gazette_date_start/end
+- Structural filters: period, application_type, rapporteur_name, norm_type, review_outcomes, reason_for_final_outcome
+- Boolean filters: has_press_release, has_dissenting_opinion, has_different_reasoning
+- Other: basis_constitution_article_numbers, attending_members_names
+- Pagination: results_per_page (1-10), page_to_fetch, sort_by_criteria
+
+**Bireysel Başvuru Search Parameters (simple):**
+- keywords[] (AND logic only)
+- page_to_fetch for pagination
+
+**Search Architecture (client.py):**
+- `_build_search_query_params_for_aym()`: Converts Pydantic model to URL query parameters (tuples list)
+- `search_norm_denetimi_decisions()`: Makes HTTP GET request with params, parses HTML response
+- Uses BeautifulSoup to find:
+ - Decision count: div.bulunankararsayisi (regex: "(\d+)\s*Karar Bulundu")
+ - Individual decisions: div.birkarar (contains reference number, metadata, keyword count)
+ - Decision details: Next sibling div.col-sm-12 with table containing norm information
+- Returns AnayasaSearchResult with parsed decisions list
+
+### 4. Document Retrieval (Full Text Conversion)
+
+**HTML to Markdown Conversion Process:**
+1. Fetch document from URL
+2. Parse HTML with BeautifulSoup
+3. Extract main content:
+ - Find div#Karar (decision tab) or fallback to div.KararMetni or div.WordSection1
+ - Remove: scripts, styles, .item.col-sm-12 divs, .modal.fade divs
+4. Convert to Markdown using MarkItDown with BytesIO stream (no temp files)
+5. Extract metadata during fetch:
+ - Esas No./Karar No.: Find bold text in
tags containing "Esas No.:" and "Karar No.:"
+ - Karar Tarihi: Find bold text containing "Karar tarihi:" or regex "Karar Tarihi\s*:\s*([\d\.]+)"
+ - Resmi Gazete: Find text containing "Resmî Gazete tarih ve sayısı:" or "Resmi Gazete tarih/sayı:"
+
+**Pagination & Chunking:**
+- Split markdown into 5,000 character chunks
+- Calculate: total_pages = ceil(len(markdown) / 5000)
+- Return current_page_clamped (max 1, min total_pages)
+- Include pagination metadata: current_page, total_pages, is_paginated flag
+
+### 5. Data Models (models.py - 230 lines)
+
+**Norm Denetimi Models:**
+- `AnayasaNormDenetimiSearchRequest`: 19 search parameters
+- `AnayasaReviewedNormInfo`: norm_name_or_number, article_number, review_type_and_outcome, outcome_reason, basis_constitution_articles_cited[], postponement_period
+- `AnayasaDecisionSummary`: decision_reference_no, decision_page_url, keywords_found_count, application_type_summary, applicant_summary, decision_outcome_summary, decision_date_summary, reviewed_norms[]
+- `AnayasaSearchResult`: decisions[], total_records_found, retrieved_page_number
+- `AnayasaDocumentMarkdown`: source_url, decision_reference_no_from_page, decision_date_from_page, official_gazette_info_from_page, markdown_chunk, current_page, total_pages, is_paginated
+
+**Bireysel Başvuru Models:**
+- `AnayasaBireyselReportSearchRequest`: keywords[], page_to_fetch
+- `AnayasaBireyselReportDecisionDetail`: hak, mudahale_iddiası, sonuç, giderim (4 fields per right examined)
+- `AnayasaBireyselReportDecisionSummary`: title, decision_reference_no, decision_page_url, decision_type_summary, decision_making_body, application_date_summary, decision_date_summary, application_subject_summary, details[]
+- `AnayasaBireyselReportSearchResult`: decisions[], total_records_found, retrieved_page_number
+- `AnayasaBireyselBasvuruDocumentMarkdown`: source_url, basvuru_no_from_page, karar_tarihi_from_page, basvuru_tarihi_from_page, karari_veren_birim_from_page, karar_turu_from_page, resmi_gazete_info_from_page, markdown_chunk, current_page, total_pages, is_paginated
+
+**Unified Models:**
+- `AnayasaUnifiedSearchRequest`: decision_type (norm_denetimi|bireysel_basvuru), keywords[], page_to_fetch, results_per_page, + type-specific parameters
+- `AnayasaUnifiedSearchResult`: decision_type, decisions[] (Dict[str, Any]), total_records_found, retrieved_page_number
+- `AnayasaUnifiedDocumentMarkdown`: decision_type, source_url, document_data (Dict), markdown_chunk, current_page, total_pages, is_paginated
+
+### 6. Unified Client Routing (unified_client.py - 122 lines)
+
+**AnayasaUnifiedClient class:**
+- Maintains instances of both norm_client and bireysel_client
+- `search_unified()`: Routes based on decision_type parameter
+ - norm_denetimi: Converts to AnayasaNormDenetimiSearchRequest, calls norm_client.search_norm_denetimi_decisions()
+ - bireysel_basvuru: Converts to AnayasaBireyselReportSearchRequest, calls bireysel_client.search_bireysel_basvuru_report()
+ - Returns unified AnayasaUnifiedSearchResult
+- `get_document_unified()`: Auto-detects decision type from URL
+ - Checks for "normkararlarbilgibankasi" in netloc or "/ND/" in path → norm_denetimi
+ - Checks for "kararlarbilgibankasi" in netloc or "/BB/" in path → bireysel_basvuru
+ - Calls appropriate client, wraps result in unified model
+
+### 7. MCP Tool Integration (mcp_server_main.py)
+
+**Active Tools (2 tools - Phase 6 optimization):**
+
+```python
+@app.tool(
+ description="Search Constitutional Court decisions from either Norm Control or Individual Applications",
+ annotations={"readOnlyHint": True, "openWorldHint": True, "idempotentHint": True}
+)
+async def search_anayasa_unified(
+ decision_type: Literal["norm_denetimi", "bireysel_basvuru"],
+ keywords: List[str],
+ page_to_fetch: int (1-100),
+ # Norm Denetimi specific (ignored for bireysel_basvuru)
+ keywords_all: List[str],
+ keywords_any: List[str],
+ decision_type_norm: Literal["ALL", "1", "2", "3"],
+ application_date_start: str,
+ application_date_end: str,
+ # Bireysel Başvuru specific (ignored for norm_denetimi)
+ decision_start_date: str,
+ decision_end_date: str,
+ norm_type: Literal["ALL", "1", "2", ...],
+ subject_category: str
+) -> str (JSON)
+```
+
+```python
+@app.tool(
+ description="Retrieve full text of Constitutional Court decision. Auto-detects decision type from URL",
+ annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True}
+)
+async def get_anayasa_document_unified(
+ document_url: str,
+ page_number: int (1-indexed)
+) -> str (JSON)
+```
+
+**Deactivated Tools (4 tools - Phase 6 optimization, marked with DEACTIVATED):**
+- search_anayasa_norm_denetimi_decisions
+- get_anayasa_norm_denetimi_document_markdown
+- search_anayasa_bireysel_basvuru_report
+- get_anayasa_bireysel_basvuru_document_markdown
+
+## Search Capabilities Analysis
+
+### Current Keyword-Based Search Strengths
+
+**Norm Denetimi - Rich Structural Filtering:**
+1. Multi-keyword logic with AND/OR/NOT operators
+2. Case/decision number search (exact matching)
+3. Date range filtering (review, decision, gazette dates)
+4. Norm categorization (14 norm types)
+5. Application type filtering (3 categories)
+6. Constitutional period selection (1961 vs 1982 constitutions)
+7. Decision outcome filtering (8 outcome types)
+8. Reasoning/grounds filtering (30 different grounds)
+9. Member/rapporteur filtering
+10. Constitutional articles cited filtering
+
+**Bireysel Başvuru - Report Format:**
+1. Simple keyword search
+2. Rights/claims detailed in structured table format
+3. Remedy/solution tracking
+
+### Limitations of Current Keyword Search
+
+1. **No semantic understanding**: Different words for same concept ("mülkiyet hakkı" vs "property rights")
+2. **No concept hierarchy**: Can't find related legal principles
+3. **No cross-language**: Turkish-only, no English queries
+4. **No abbreviation matching**: "HADD" vs "Hukuk Alanında Değerli Dosya Denetimi"
+5. **No synonym support**: Formal vs informal terminology
+6. **No semantic similarity**: Can't find similar cases with different terminology
+7. **No legal concept graph**: Can't traverse related principles or doctrines
+8. **No fuzzy matching**: Typos or spelling variations fail completely
+9. **No legal reasoning search**: Can't query by legal arguments or doctrinal approaches
+10. **No cross-system semantic linking**: Norm Denetimi and Bireysel Başvuru not semantically linked
+11. **Order dependency**: Query order may affect results
+12. **No ranking by relevance**: Just keyword presence/absence
+13. **No query expansion**: No automatic synonym/related term expansion
+
+### HTML Document Structure
+
+**Norm Denetimi Search Results HTML:**
+```
+div.birkarar (repeated for each decision)
+├── div.bkararbaslik (header with E./K. numbers)
+│ └── div.BulunanKelimeSayisi (keyword count)
+└── div.kararbilgileri (metadata with | separators: application_type|applicant|outcome|date)
+
+Next sibling:
+div.col-sm-12
+└── table.table > tbody > tr (one row per reviewed norm with 6 columns)
+ ├── td: norm name/number
+ ├── td: article number
+ ├── td: review type and outcome
+ ├── td: outcome reason
+ ├── td: constitutional articles cited (comma-separated)
+ └── td: postponement period
+```
+
+**Full Decision Content (both types):**
+```
+div#Karar (decision tab)
+└── div.KararMetni or div.WordSection1
+ └── HTML content in MS Word format (many nested divs with styles)
+
+Metadata extracted from:
+
Esas No.: [number]
+Karar No.: [number]
+Karar tarihi: [date]
+Resmî Gazete tarih ve sayısı: [info]
+```
+
+### Document Content Characteristics
+
+- **Language**: Turkish legal language (specialized terminology)
+- **Format**: Microsoft Word-generated HTML (nested divs, complex styles)
+- **Content types**:
+ - Norm Denetimi: Constitutional principle analysis, legal reasoning, comparison with challenged norm
+ - Bireysel Başvuru: Right violated, remedy granted, procedural requirements
+- **Typical length**: 5,000-50,000+ characters
+- **Citations**: Internal cross-references to constitutional articles
+- **Structure**: Formal legal document with sections, subsections, reasoning
+
+## Key Technical Insights for Semantic Search
+
+### Content Encoding
+- Currently: HTML → BeautifulSoup parsing → MarkItDown → Markdown
+- Extraction: Specific div/class/id selectors
+- Metadata: Regex patterns and text parsing
+
+### Search Query Flow
+1. User provides keywords/filters
+2. Convert Pydantic model to URL query parameters
+3. HTTP GET request to Constitutional Court API
+4. HTML response parsed with BeautifulSoup
+5. Decision summaries extracted and validated
+6. Results returned as JSON
+
+### Document Retrieval Flow
+1. Get document URL from search results
+2. HTTP GET request to URL
+3. Parse HTML for metadata extraction
+4. MarkItDown converts HTML to Markdown
+5. Chunk by 5,000 characters
+6. Return paginated Markdown with metadata
+
+## Performance Baseline
+
+- **Search**: ~1-5 seconds (HTML parsing + regex extraction)
+- **Document**: ~2-10 seconds (fetch + parse + MarkItDown + chunking)
+- **Memory**: Minimal (5,000 char chunks, no full document in memory)
+- **API Response Size**: Typically 50-500 KB HTML for search, 100-1000 KB for full decision
+
+## Next Steps for Semantic Search Integration
+
+1. **Vector Embeddings**: Embed decisions using Turkish legal model
+2. **Concept Extraction**: Identify and tag legal concepts (rights, procedures, principles)
+3. **Semantic Queries**: Convert natural language questions to embeddings
+4. **Hybrid Search**: Combine keyword + semantic similarity
+5. **Legal Ontology**: Map Turkish Constitutional Court concepts and relationships
+6. **Cross-system Linking**: Semantically link Norm Denetimi and Bireysel Başvuru decisions
+7. **Precedent Graph**: Extract citations and create legal precedent relationships
+8. **Fine-tuned Embeddings**: Train embeddings specifically on Turkish Constitutional law
+9. **Ranking**: Re-rank results by semantic relevance to user's legal intent
+10. **Explanation**: Provide semantic reasoning for why result is relevant
diff --git a/.serena/project.yml b/.serena/project.yml
index db1e992..f340017 100644
--- a/.serena/project.yml
+++ b/.serena/project.yml
@@ -79,6 +79,61 @@ excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
-
+# the name by which the project can be referenced within Serena
project_name: "yargi-mcp"
+
+# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []
+
+# list of mode names to that are always to be included in the set of active modes
+# The full set of modes to be activated is base_modes + default_modes.
+# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
+# Otherwise, this setting overrides the global configuration.
+# Set this to [] to disable base modes for this project.
+# Set this to a list of mode names to always include the respective modes for this project.
+base_modes:
+
+# list of mode names that are to be activated by default.
+# The full set of modes to be activated is base_modes + default_modes.
+# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
+# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
+# This setting can, in turn, be overridden by CLI parameters (--mode).
+default_modes:
+
+# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
+# This cannot be combined with non-empty excluded_tools or included_optional_tools.
+fixed_tools: []
+
+# override of the corresponding setting in serena_config.yml, see the documentation there.
+# If null or missing, the value from the global config is used.
+symbol_info_budget:
+
+# The language backend to use for this project.
+# If not set, the global setting from serena_config.yml is used.
+# Valid values: LSP, JetBrains
+# Note: the backend is fixed at startup. If a project with a different backend
+# is activated post-init, an error will be returned.
+language_backend:
+
+# list of regex patterns which, when matched, mark a memory entry as read‑only.
+# Extends the list from the global configuration, merging the two lists.
+read_only_memory_patterns: []
+
+# line ending convention to use when writing source files.
+# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
+# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
+line_ending:
+
+# list of regex patterns for memories to completely ignore.
+# Matching memories will not appear in list_memories or activate_project output
+# and cannot be accessed via read_memory or write_memory.
+# To access ignored memory files, use the read_file tool on the raw file path.
+# Extends the list from the global configuration, merging the two lists.
+# Example: ["_archive/.*", "_episodes/.*"]
+ignored_memory_patterns: []
+
+# advanced configuration option allowing to configure language server-specific options.
+# Maps the language key to the options.
+# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
+# No documentation on options means no options are available.
+ls_specific_settings: {}
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 489ece5..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,34 +0,0 @@
-# -------- BASE IMAGE ---------------------------------------------------------
-FROM python:3.12-slim
-
-# -------- 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 . && \
- uv pip install --system --no-cache-dir .[asgi,saas]
-
-# Cache buster - force rebuild
-ARG CACHE_BUST=202510061202
-RUN echo "Cache bust: $CACHE_BUST"
-
-# 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/Procfile b/Procfile
deleted file mode 100644
index 2b6b125..0000000
--- a/Procfile
+++ /dev/null
@@ -1 +0,0 @@
-web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT
\ No newline at end of file
diff --git a/asgi_app.py b/asgi_app.py
index 92b0c34..219608d 100755
--- a/asgi_app.py
+++ b/asgi_app.py
@@ -2,112 +2,32 @@
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 OAuth integration and proper middleware support.
+allowing it to be deployed as a web service with FastAPI wrapper.
Usage:
uvicorn asgi_app:app --host 0.0.0.0 --port 8000
"""
import os
-import time
-import logging
import json
-from datetime import datetime, timedelta
-from fastapi import FastAPI, Request, HTTPException, Query
-from fastapi.responses import JSONResponse, HTMLResponse, Response
-from fastapi.exception_handlers import http_exception_handler
+import logging
+from fastapi import FastAPI, Request
+from fastapi.responses import JSONResponse
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
-from starlette.middleware.base import BaseHTTPMiddleware
-# Import the proper create_app function that includes all middleware
from mcp_server_main import create_app
-# Conditional auth-related imports (only if auth enabled)
-_auth_check = os.getenv("ENABLE_AUTH", "false").lower() == "true"
-
-if _auth_check:
- mcp_auth_router = None
-
- # Import Stripe webhook router
- try:
- from stripe_webhook import router as stripe_router
- except ImportError:
- stripe_router = None
-else:
- mcp_auth_router = None
- stripe_router = None
-
-# OAuth configuration from environment variables
-CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com")
-BASE_URL = os.getenv("BASE_URL", "https://api.yargimcp.com")
-CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
-CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
-
# Setup logging
logger = logging.getLogger(__name__)
-# Configure CORS and Auth middleware
+# Configure CORS
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
-# Configure Bearer token authentication based on ENABLE_AUTH
-auth_enabled = os.getenv("ENABLE_AUTH", "false").lower() == "true"
-bearer_auth = None
+# Create MCP app
+mcp_server = create_app()
-# Only import and configure auth if enabled
-if auth_enabled:
- # Import FastMCP JWT Verifier (handles both old and new FastMCP versions)
- try:
- # FastMCP 2.12+ uses JWTVerifier
- from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
- AuthProviderClass = JWTVerifier
- except ImportError:
- try:
- # Older FastMCP versions used BearerAuthProvider
- from fastmcp.server.auth import BearerAuthProvider
- from fastmcp.server.auth.providers.bearer import RSAKeyPair
- AuthProviderClass = BearerAuthProvider
- except ImportError:
- logger.error("No compatible auth provider found in FastMCP")
- AuthProviderClass = None
- RSAKeyPair = None
-
- # Import Clerk SDK at module level for performance
- try:
- from clerk_backend_api import Clerk
- CLERK_SDK_AVAILABLE = True
- except ImportError:
- CLERK_SDK_AVAILABLE = False
- logger.warning("Clerk SDK not available - falling back to development mode")
-
- if AuthProviderClass:
- if CLERK_SECRET_KEY and CLERK_ISSUER:
- # Production: Use Clerk JWKS endpoint for token validation
- bearer_auth = AuthProviderClass(
- jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
- issuer=None,
- algorithm="RS256",
- audience=None,
- required_scopes=[]
- )
- elif RSAKeyPair:
- # Development: Generate RSA key pair for testing
- dev_key_pair = RSAKeyPair.generate()
- bearer_auth = AuthProviderClass(
- public_key=dev_key_pair.public_key,
- issuer="https://dev.yargimcp.com",
- audience="dev-mcp-server",
- required_scopes=["yargi.read"]
- )
-else:
- CLERK_SDK_AVAILABLE = False
- logger.info("Authentication disabled (ENABLE_AUTH=false)")
-
-# Create MCP app with Bearer authentication (None if auth disabled)
-mcp_server = create_app(auth=bearer_auth)
-
-# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
+# Create MCP Starlette sub-application
mcp_app = mcp_server.http_app(path="/")
@@ -118,7 +38,7 @@ class UTF8JSONResponse(JSONResponse):
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,
@@ -133,43 +53,22 @@ custom_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"],
+ allow_methods=["GET", "POST", "OPTIONS"],
+ allow_headers=["Content-Type", "X-Request-ID", "X-Session-ID"],
),
]
# Create FastAPI wrapper application
app = FastAPI(
title="Yargı MCP Server",
- description="MCP server for Turkish legal databases with OAuth authentication",
+ description="MCP server for Turkish legal databases",
version="0.1.0",
middleware=custom_middleware,
- default_response_class=UTF8JSONResponse, # Use UTF-8 JSON encoder
- redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint
+ default_response_class=UTF8JSONResponse,
+ redirect_slashes=False,
)
-# Add auth-related routers to FastAPI (only if available)
-if stripe_router:
- app.include_router(stripe_router, prefix="/api/stripe")
-# 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"""
@@ -178,119 +77,33 @@ async def health_check():
"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)
-# MCP mount at /mcp handles path routing correctly
-# IMPORTANT: Add FastAPI endpoints BEFORE mounting MCP app
-# Otherwise mount at root will catch all requests
-
-# Debug endpoint to test routing
-@app.get("/debug/test")
-async def debug_test():
- """Debug endpoint to test if FastAPI routes work"""
- return {"message": "FastAPI routes working", "debug": True}
-
-# Clerk CORS proxy endpoints
-@app.api_route("/clerk-proxy/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
-async def clerk_cors_proxy(request: Request, path: str):
- """
- Proxy requests to Clerk to bypass CORS restrictions.
- Forwards requests from Claude AI to clerk.yargimcp.com with proper CORS headers.
- """
- import httpx
-
- # Build target URL
- clerk_url = f"https://clerk.yargimcp.com/{path}"
-
- # Forward query parameters
- if request.url.query:
- clerk_url += f"?{request.url.query}"
-
- # Copy headers (exclude host/origin)
- headers = dict(request.headers)
- headers.pop('host', None)
- headers.pop('origin', None)
- headers['origin'] = 'https://yargimcp.com' # Use our frontend domain
-
- try:
- async with httpx.AsyncClient() as client:
- # Forward the request to Clerk
- if request.method == "OPTIONS":
- # Handle preflight
- response = await client.request(
- method=request.method,
- url=clerk_url,
- headers=headers
- )
- else:
- # Forward body for POST/PUT requests
- body = None
- if request.method in ["POST", "PUT", "PATCH"]:
- body = await request.body()
-
- response = await client.request(
- method=request.method,
- url=clerk_url,
- headers=headers,
- content=body
- )
-
- # Create response with CORS headers
- response_headers = dict(response.headers)
- response_headers.update({
- "Access-Control-Allow-Origin": "*",
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
- "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, Origin, X-Requested-With",
- "Access-Control-Allow-Credentials": "true",
- "Access-Control-Max-Age": "86400"
- })
-
- return Response(
- content=response.content,
- status_code=response.status_code,
- headers=response_headers,
- media_type=response.headers.get("content-type")
- )
-
- except Exception as e:
- return JSONResponse(
- {"error": "proxy_error", "message": str(e)},
- status_code=500,
- headers={"Access-Control-Allow-Origin": "*"}
- )
-
-# FastAPI root endpoint
@app.get("/")
async def root():
"""Root endpoint with service information"""
return {
"service": "Yargı MCP Server",
- "description": "MCP server for Turkish legal databases with OAuth authentication",
+ "description": "MCP server for Turkish legal databases",
"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)",
+ "Danıştay (Council of State)",
"Emsal (Precedent)",
"Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes)",
"Anayasa Mahkemesi (Constitutional Court)",
@@ -299,140 +112,12 @@ async def root():
"Sayıştay (Court of Accounts)",
"KVKK (Personal Data Protection Authority)",
"BDDK (Banking Regulation and Supervision Agency)",
- "Bedesten API (Multiple courts)"
+ "Bedesten API (Multiple courts)",
+ "Sigorta Tahkim Komisyonu (Insurance Arbitration Commission)",
],
- "authentication": {
- "enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true",
- "type": "OAuth 2.0 via Clerk",
- "issuer": CLERK_ISSUER,
- "providers": ["google"],
- "flow": "authorization_code"
- }
}
-# OAuth 2.0 Authorization Server Metadata - MCP standard location
-@app.get("/.well-known/oauth-authorization-server")
-async def oauth_authorization_server_root():
- """OAuth 2.0 Authorization Server Metadata - root level for compatibility"""
- return {
- "issuer": BASE_URL, # Use BASE_URL as issuer for MCP integration
- "authorization_endpoint": f"{BASE_URL}/auth/login",
- "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 - suffix versions
-@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 {
- "issuer": BASE_URL, # Use BASE_URL as issuer for MCP integration
- "authorization_endpoint": f"{BASE_URL}/auth/login",
- "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 {
- "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"
- }
-
-# 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 {
- "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 {
- "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 {
- "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": "/token",
- "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"""
@@ -442,159 +127,20 @@ async def status():
"name": tool.name,
"description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description
})
-
+
return {
"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"
}
-# Simplified OAuth session validation for callback endpoints only
-async def validate_clerk_session_for_oauth(request: Request, clerk_token: str = None) -> str:
- """Validate Clerk session for OAuth callback endpoints only (not for MCP endpoints)"""
-
- try:
- # Use Clerk SDK if available
- if not CLERK_SDK_AVAILABLE:
- raise ImportError("Clerk SDK not available")
- clerk = Clerk(bearer_auth=CLERK_SECRET_KEY)
-
- # Try JWT token first (from URL parameter)
- if clerk_token:
- try:
- return "oauth_user_from_token"
- except Exception as e:
- pass
- # Fallback to cookie validation
- clerk_session = request.cookies.get("__session")
- if not clerk_session:
- raise HTTPException(status_code=401, detail="No Clerk session found")
-
- # Validate session with Clerk
- session = clerk.sessions.verify_session(clerk_session)
- return session.user_id
-
- except ImportError:
- return "dev_user_123"
- except Exception as e:
- raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}")
-
-# MCP OAuth Callback Endpoint
-@app.get("/auth/mcp-callback")
-async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
- """Handle OAuth callback for MCP token generation"""
-
- try:
- # Validate Clerk session with JWT token support
- user_id = await validate_clerk_session_for_oauth(request, clerk_token)
-
- # 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:
- 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:
- 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_for_oauth(request)
-
- return {
- "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}
- )
-
-# Mount MCP app at /mcp/ with trailing slash
+# Mount MCP app at /mcp/
app.mount("/mcp/", mcp_app)
# Set the lifespan context after mounting
app.router.lifespan_context = mcp_app.lifespan
# Export for uvicorn
-__all__ = ["app"]
\ No newline at end of file
+__all__ = ["app"]
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index d36eee7..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-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/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
deleted file mode 100644
index db5f361..0000000
--- a/docs/DEPLOYMENT.md
+++ /dev/null
@@ -1,428 +0,0 @@
-# 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/fly-no-auth.toml b/fly-no-auth.toml
deleted file mode 100644
index 5205913..0000000
--- a/fly-no-auth.toml
+++ /dev/null
@@ -1,46 +0,0 @@
-# fly.toml app configuration file for yargi-mcp-noauth
-#
-# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
-#
-
-app = 'yargi-mcp-free'
-primary_region = 'fra'
-
-[env]
-ENABLE_AUTH = "false"
-HOST = "0.0.0.0"
-PORT = "8000"
-LOG_LEVEL = "info"
-
-[build]
-
-[http_service]
- internal_port = 8000
- force_https = true
- auto_stop_machines = 'off'
- auto_start_machines = true
- min_machines_running = 1
- processes = ['app']
-
- # Enable connection persistence for MCP sessions
- [http_service.concurrency]
- type = "connections"
- hard_limit = 100
- soft_limit = 80
-
-[[vm]]
- memory = '1gb'
- cpu_kind = 'shared'
- cpus = 1
-
-[deploy]
- strategy = "immediate"
-
-[processes]
- app = "python asgi_app.py"
-
-[checks.http_health] # keep MCP /health live
- type = "http"
- interval = "30s"
- timeout = "10s"
- path = "/health"
\ No newline at end of file
diff --git a/fly.toml b/fly.toml
deleted file mode 100644
index fa1cc3f..0000000
--- a/fly.toml
+++ /dev/null
@@ -1,40 +0,0 @@
-# fly.toml app configuration file generated for yargi-mcp on 2025-06-29T00:23:47+03:00
-#
-# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
-#
-
-app = 'yargi-mcp'
-primary_region = 'fra'
-
-[env]
-ENABLE_AUTH = "true"
-HOST = "0.0.0.0"
-PORT = "8000"
-LOG_LEVEL = "info"
-
-[build]
-
-[http_service]
- internal_port = 8000
- force_https = true
- auto_stop_machines = 'off'
- auto_start_machines = true
- min_machines_running = 1
- processes = ['app']
-
- # Enable connection persistence for MCP sessions
- [http_service.concurrency]
- type = "connections"
- hard_limit = 100
- soft_limit = 80
-
-[[vm]]
- memory = '1gb'
- cpu_kind = 'shared'
- cpus = 1
-
-[checks.http_health] # keep MCP /health live
- type = "http"
- interval = "30s"
- timeout = "10s"
- path = "/health"
diff --git a/mcp_auth/__init__.py b/mcp_auth/__init__.py
deleted file mode 100644
index 0ed22c9..0000000
--- a/mcp_auth/__init__.py
+++ /dev/null
@@ -1,28 +0,0 @@
-"""
-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/mcp_auth/clerk_config.py b/mcp_auth/clerk_config.py
deleted file mode 100644
index 2c3e205..0000000
--- a/mcp_auth/clerk_config.py
+++ /dev/null
@@ -1,73 +0,0 @@
-"""
-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/mcp_auth/middleware.py b/mcp_auth/middleware.py
deleted file mode 100644
index 0dc6301..0000000
--- a/mcp_auth/middleware.py
+++ /dev/null
@@ -1,315 +0,0 @@
-"""
-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/mcp_auth/oauth.py b/mcp_auth/oauth.py
deleted file mode 100644
index efc3adc..0000000
--- a/mcp_auth/oauth.py
+++ /dev/null
@@ -1,304 +0,0 @@
-"""
-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/mcp_auth/policy.py b/mcp_auth/policy.py
deleted file mode 100644
index e947af3..0000000
--- a/mcp_auth/policy.py
+++ /dev/null
@@ -1,201 +0,0 @@
-"""
-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/mcp_auth/storage.py b/mcp_auth/storage.py
deleted file mode 100644
index ea1ad8f..0000000
--- a/mcp_auth/storage.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""
-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/mcp_auth_factory.py b/mcp_auth_factory.py
deleted file mode 100644
index a5760f6..0000000
--- a/mcp_auth_factory.py
+++ /dev/null
@@ -1,193 +0,0 @@
-"""
-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/mcp_auth_http_adapter.py b/mcp_auth_http_adapter.py
deleted file mode 100644
index 442b540..0000000
--- a/mcp_auth_http_adapter.py
+++ /dev/null
@@ -1,383 +0,0 @@
-"""
-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/mcp_server_main.py b/mcp_server_main.py
index bb1c583..cd67a50 100644
--- a/mcp_server_main.py
+++ b/mcp_server_main.py
@@ -31,7 +31,6 @@ try:
except ImportError:
TIKTOKEN_AVAILABLE = False
tiktoken = None
-from fastmcp.server.dependencies import get_access_token, AccessToken
from fastmcp import Context
# Use standard exception for tool errors
@@ -241,14 +240,10 @@ class TokenCountingMiddleware(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."""
+def create_app():
+ """Create FastMCP app with standard capabilities."""
global app
- if auth:
- app.auth = auth
- logger.info("MCP server created with Bearer authentication enabled")
- else:
- logger.info("MCP server created with standard capabilities...")
+ logger.info("MCP server created with standard capabilities...")
# Add token counting middleware only if tiktoken is available
if TIKTOKEN_AVAILABLE:
@@ -1125,24 +1120,6 @@ For best results, use exact phrases with quotes for legal terms."""),
) -> 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 - DISABLED: Already handled by Bearer auth provider
- # 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
# Convert date formats if provided
diff --git a/nginx.conf b/nginx.conf
deleted file mode 100644
index 5cb39b2..0000000
--- a/nginx.conf
+++ /dev/null
@@ -1,94 +0,0 @@
-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/pyproject.toml b/pyproject.toml
index 694b64d..b1a8bbe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,22 +46,15 @@ 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",
- "tiktoken>=0.5.0",
- "PyJWT>=2.8.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"]
+py-modules = ["mcp_server_main", "asgi_app"]
[tool.setuptools.packages.find]
-include = ["*_mcp_module", "mcp_auth", "semantic_search"]
+include = ["*_mcp_module", "semantic_search"]
[build-system]
requires = ["setuptools>=65.0", "wheel"]
diff --git a/run_asgi.py b/run_asgi.py
deleted file mode 100644
index 30ef9f3..0000000
--- a/run_asgi.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python3
-"""
-Standalone ASGI server runner for Yargı MCP
-
-This script provides a simple way to run the Yargı MCP server
-as a web service using uvicorn.
-
-Usage:
- python run_asgi.py
- python run_asgi.py --host 0.0.0.0 --port 8080
- python run_asgi.py --reload # For development
-"""
-
-import os
-import sys
-import argparse
-import logging
-from pathlib import Path
-
-# Add project root to Python path
-sys.path.insert(0, str(Path(__file__).parent))
-
-try:
- import uvicorn
-except ImportError:
- print("Error: uvicorn is not installed.")
- print("Please install it with: pip install uvicorn")
- sys.exit(1)
-
-# Configure logging
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-
-def main():
- parser = argparse.ArgumentParser(
- description="Run Yargı MCP server as an ASGI web service"
- )
- parser.add_argument(
- "--host",
- type=str,
- default=os.getenv("HOST", "127.0.0.1"),
- help="Host to bind to (default: 127.0.0.1)"
- )
- parser.add_argument(
- "--port",
- type=int,
- default=int(os.getenv("PORT", "8000")),
- help="Port to bind to (default: 8000)"
- )
- parser.add_argument(
- "--reload",
- action="store_true",
- help="Enable auto-reload for development"
- )
- parser.add_argument(
- "--transport",
- choices=["http", "sse"],
- default="http",
- help="Transport type (default: http)"
- )
- parser.add_argument(
- "--log-level",
- choices=["debug", "info", "warning", "error"],
- default=os.getenv("LOG_LEVEL", "info").lower(),
- help="Log level (default: info)"
- )
- parser.add_argument(
- "--workers",
- type=int,
- default=1,
- help="Number of worker processes (default: 1)"
- )
-
- args = parser.parse_args()
-
- # Select app based on transport
- app_name = "asgi_app:app" if args.transport == "http" else "asgi_app:sse_app"
-
- # Configure uvicorn
- config = {
- "app": app_name,
- "host": args.host,
- "port": args.port,
- "log_level": args.log_level,
- "reload": args.reload,
- "access_log": True,
- }
-
- # Add workers only if not in reload mode
- if not args.reload and args.workers > 1:
- config["workers"] = args.workers
-
- # Print startup information
- print(f"Starting Yargı MCP server...")
- print(f"Host: {args.host}")
- print(f"Port: {args.port}")
- print(f"Transport: {args.transport}")
- print(f"Log level: {args.log_level}")
- if args.reload:
- print("Auto-reload: enabled")
- else:
- print(f"Workers: {args.workers}")
- print(f"\nServer will be available at: http://{args.host}:{args.port}")
- print(f"MCP endpoint: http://{args.host}:{args.port}/mcp/")
- print(f"Health check: http://{args.host}:{args.port}/health")
- print(f"API status: http://{args.host}:{args.port}/status")
- print("\nPress CTRL+C to stop the server\n")
-
- # Run uvicorn
- try:
- uvicorn.run(**config)
- except KeyboardInterrupt:
- print("\nShutting down server...")
- sys.exit(0)
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/starlette_app.py b/starlette_app.py
deleted file mode 100644
index a9e711f..0000000
--- a/starlette_app.py
+++ /dev/null
@@ -1,159 +0,0 @@
-"""
-Starlette integration example for Yargı MCP Server
-
-This module demonstrates how to integrate the Yargı MCP server
-with a Starlette application, including authentication middleware
-and custom routing.
-
-Usage:
- uvicorn starlette_app:app --host 0.0.0.0 --port 8000
-"""
-
-import os
-from starlette.applications import Starlette
-from starlette.routing import Mount, Route
-from starlette.requests import Request
-from starlette.responses import JSONResponse, PlainTextResponse, RedirectResponse
-from starlette.middleware import Middleware
-from starlette.middleware.cors import CORSMiddleware
-from starlette.middleware.authentication import AuthenticationMiddleware
-from starlette.authentication import (
- AuthenticationBackend, AuthCredentials, SimpleUser, AuthenticationError
-)
-
-# Import the main MCP app
-from mcp_server_main import app as mcp_server
-
-# Simple token authentication backend
-class TokenAuthBackend(AuthenticationBackend):
- async def authenticate(self, request):
- auth_header = request.headers.get("Authorization")
- expected_token = os.getenv("API_TOKEN")
-
- # Skip auth for health check and public endpoints
- if request.url.path in ["/health", "/", "/login"]:
- return None
-
- if not expected_token:
- # No token configured, allow all
- return AuthCredentials(["authenticated"]), SimpleUser("anonymous")
-
- if not auth_header:
- raise AuthenticationError("Authorization header required")
-
- try:
- scheme, token = auth_header.split()
- if scheme.lower() != "bearer":
- raise AuthenticationError("Invalid authentication scheme")
-
- if token != expected_token:
- raise AuthenticationError("Invalid token")
-
- return AuthCredentials(["authenticated"]), SimpleUser("user")
- except ValueError:
- raise AuthenticationError("Invalid authorization header format")
-
-# Homepage
-async def homepage(request: Request):
- return JSONResponse({
- "service": "Yargı MCP Server",
- "version": "0.1.0",
- "endpoints": {
- "mcp": "/mcp-server/mcp/",
- "api": "/api/",
- "health": "/health"
- }
- })
-
-# API info endpoint
-async def api_info(request: Request):
- if not request.user.is_authenticated:
- return JSONResponse({"error": "Authentication required"}, status_code=401)
-
- return JSONResponse({
- "authenticated_as": request.user.display_name,
- "available_tools": len(mcp_server._tool_manager._tools),
- "databases": [
- "Yargıtay", "Danıştay", "Emsal", "Uyuşmazlık",
- "Anayasa", "KIK", "Rekabet", "Bedesten"
- ]
- })
-
-# Health check
-async def health_check(request: Request):
- return JSONResponse({
- "status": "healthy",
- "service": "Yargı MCP Server"
- })
-
-# Login example (returns token for demo)
-async def login(request: Request):
- token = os.getenv("API_TOKEN", "demo-token")
- return JSONResponse({
- "message": "Use this token in Authorization header",
- "example": f"Authorization: Bearer {token}",
- "note": "Set API_TOKEN environment variable to change token"
- })
-
-# Create MCP ASGI app
-mcp_app = mcp_server.http_app(path='/mcp')
-
-# Configure middleware
-middleware = [
- Middleware(
- CORSMiddleware,
- allow_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","),
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
- ),
- Middleware(AuthenticationMiddleware, backend=TokenAuthBackend()),
-]
-
-# Create routes
-routes = [
- Route("/", homepage),
- Route("/health", health_check),
- Route("/login", login),
- Route("/api/info", api_info),
- Mount("/mcp-server", app=mcp_app),
-]
-
-# Create Starlette app
-app = Starlette(
- routes=routes,
- middleware=middleware,
- lifespan=mcp_app.lifespan
-)
-
-# Nested mount example
-def create_nested_app():
- """Example of nested mounting for complex routing structures"""
-
- # Create inner app with MCP
- inner_app = Starlette(
- routes=[Mount("/services", app=mcp_app)],
- middleware=middleware
- )
-
- # Create outer app
- outer_app = Starlette(
- routes=[
- Route("/", homepage),
- Mount("/v1", app=inner_app),
- ],
- lifespan=mcp_app.lifespan
- )
-
- # MCP would be available at /v1/services/mcp/
- return outer_app
-
-# Export both apps
-nested_app = create_nested_app()
-
-if __name__ == "__main__":
- import uvicorn
- print("Starting Starlette app with authentication...")
- print("Set API_TOKEN environment variable to enable authentication")
- print("Example: API_TOKEN=secret-token python starlette_app.py")
- uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/stripe_webhook.py b/stripe_webhook.py
deleted file mode 100644
index 3e5dfa9..0000000
--- a/stripe_webhook.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import os, stripe
-from clerk_backend_api import Clerk # Clerk backend SDK
-from fastapi import APIRouter, Request, HTTPException
-
-router = APIRouter()
-stripe.api_key = os.getenv("STRIPE_SECRET")
-clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
-
-@router.post("/stripe/webhook")
-async def stripe_hook(req: Request):
- payload, sig = await req.body(), req.headers["stripe-signature"]
- try:
- event = stripe.Webhook.construct_event( # Stripe-recommended verify
- payload, sig, os.getenv("STRIPE_WEBHOOK_SECRET"))
- except stripe.error.SignatureVerificationError:
- raise HTTPException(400, "Bad sig")
-
- if event["type"] == "customer.subscription.updated":
- item = event["data"]["object"]["items"]["data"][0]
- plan = item["price"]["nickname"] # "Pro", "Enterprise"…
- userID = event["data"]["object"]["metadata"]["clerk_user_id"]
- clerk.users.update_user_metadata( # merge into unsafe_metadata
- userID, unsafe_metadata={"plan": plan})
- return {"ok": True}
-