15 Commits
Author SHA1 Message Date
saidsurucu f5fa0076f8 Release v0.1.6: Production deployment with full Claude AI integration 2025-07-21 21:21:19 +03:00
saidsurucu 7a346ef3f6 Update mcp_server_main.py 2025-07-21 21:11:09 +03:00
saidsurucu 217103f0b6 fix tool count issue 2025-07-21 20:46:54 +03:00
saidsurucu 1fbcb65031 Update asgi_app.py 2025-07-21 19:58:44 +03:00
saidsurucu 9e40671798 Update asgi_app.py 2025-07-21 19:51:21 +03:00
saidsurucu 6c8a614872 Update asgi_app.py 2025-07-21 19:30:56 +03:00
saidsurucu 861d9e86ef Update asgi_app.py 2025-07-21 19:26:38 +03:00
saidsurucu c4b5d3608a Update asgi_app.py 2025-07-21 19:17:43 +03:00
saidsurucu 38e0cc032b Update asgi_app.py 2025-07-21 18:50:56 +03:00
saidsurucu 2c1b8c6f9d fix remote mcp 2025-07-21 18:45:20 +03:00
saidsurucu 92f04fbab6 Update asgi_app.py 2025-07-21 18:05:04 +03:00
saidsurucu 515347e29c Update asgi_app.py 2025-07-21 17:51:29 +03:00
saidsurucu ebefe22a4c Update mcp_server_main.py 2025-07-21 17:37:22 +03:00
saidsurucu c93244ee10 Update fly.toml 2025-07-21 16:44:48 +03:00
saidsurucu d84f8a2c88 update working operators 2025-07-21 15:33:16 +03:00
6 changed files with 2213 additions and 209 deletions
+2077
View File
File diff suppressed because it is too large Load Diff
+77 -189
View File
@@ -19,9 +19,10 @@ from fastapi.exception_handlers import http_exception_handler
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response
from starlette.requests import Request as StarletteRequest
# Import the fully configured MCP app with all tools
from mcp_server_main import app as mcp_server
# Import the MCP app creator function
from mcp_server_main import create_app
# Import Stripe webhook router
from stripe_webhook import router as stripe_router
@@ -36,23 +37,69 @@ BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
# Setup logging
logger = logging.getLogger(__name__)
# Configure CORS middleware
# Configure CORS and Auth middleware
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
# Import FastMCP Bearer Auth Provider
from fastmcp.server.auth import BearerAuthProvider
from fastmcp.server.auth.providers.bearer import RSAKeyPair
# Clerk JWT configuration for Bearer token validation
CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
# Configure Bearer token authentication
bearer_auth = None
if CLERK_SECRET_KEY and CLERK_ISSUER:
# Production: Use Clerk JWKS endpoint for token validation
bearer_auth = BearerAuthProvider(
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
issuer=CLERK_ISSUER,
algorithm="RS256",
audience=None, # Disable audience validation - Clerk uses different audience format
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
)
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
else:
# Development: Generate RSA key pair for testing
logger.warning("No Clerk credentials found - using development RSA key pair")
dev_key_pair = RSAKeyPair.generate()
bearer_auth = BearerAuthProvider(
public_key=dev_key_pair.public_key,
issuer="https://dev.yargimcp.com",
audience="dev-mcp-server",
required_scopes=["yargi.read"]
)
# Generate a test token for development
dev_token = dev_key_pair.create_token(
subject="dev-user",
issuer="https://dev.yargimcp.com",
audience="dev-mcp-server",
scopes=["yargi.read", "yargi.search"],
expires_in_seconds=3600 * 24 # 24 hours for development
)
logger.info(f"Development Bearer token: {dev_token}")
custom_middleware = [
Middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
),
]
# Create MCP Starlette sub-application (without auth wrapper)
mcp_app = mcp_server.http_app(
path="/",
middleware=custom_middleware
)
# Create MCP app with Bearer authentication
mcp_server = create_app(auth=bearer_auth)
# Add Starlette middleware to FastAPI (not MCP)
# MCP already has Bearer auth, no need for additional middleware on MCP level
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
mcp_app = mcp_server.http_app(path="/")
# Configure JSON encoder for proper Turkish character support
import json
@@ -74,13 +121,12 @@ class UTF8JSONResponse(JSONResponse):
separators=(",", ":"),
).encode("utf-8")
# Create FastAPI wrapper application with MCP lifespan
# Create FastAPI wrapper application
app = FastAPI(
title="Yargı MCP Server",
description="MCP server for Turkish legal databases with OAuth authentication",
version="0.1.0",
middleware=custom_middleware,
lifespan=mcp_app.lifespan, # MCP app lifespan
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
)
@@ -107,141 +153,7 @@ async def custom_401_handler(request: Request, exc: HTTPException):
return response
# Mount MCP app as sub-application at /mcp-server to avoid path conflicts
app.mount("/mcp-server", mcp_app)
# Add custom route to handle /mcp requests and forward to mounted app
@app.api_route("/mcp", methods=["POST", "DELETE", "OPTIONS"])
@app.api_route("/mcp/", methods=["POST", "DELETE", "OPTIONS"])
async def mcp_protocol_handler(request: Request):
"""Handle MCP protocol requests by forwarding to mounted app"""
# Handle DELETE requests for session termination
if request.method == "DELETE":
logger.info("DELETE request received for session termination")
# For session termination, we just return 200 OK
# The actual session cleanup is handled by the underlying MCP transport
from starlette.responses import Response
return Response(
status_code=200,
content="Session terminated successfully"
)
# REQUIRED: Validate Bearer JWT tokens for all MCP requests
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
logger.error("Missing or invalid Authorization header")
raise HTTPException(
status_code=401,
detail="Missing or invalid Authorization header. Bearer token required."
)
token = auth_header.split(" ")[1]
try:
# Check if this is a mock token for development/testing
if token.startswith("mock_clerk_jwt_"):
logger.info(f"Using mock JWT token for development: {token[:30]}...")
# For mock tokens, we'll allow access with a mock user
request.state.user_id = "mock_user_dev"
request.state.session_id = "mock_session_dev"
request.state.token_scopes = ["read", "search"]
logger.info("Mock JWT token accepted for development")
elif token.startswith("eyJ"):
# This looks like a real JWT token (starts with eyJ which is base64 encoded '{"')
logger.info(f"Processing real JWT token: {token[:30]}...")
# Validate real Clerk JWT token
from clerk_backend_api import Clerk, models
import jwt
# Decode JWT token and extract user info
try:
decoded_token = jwt.decode(token, options={"verify_signature": False})
user_id = decoded_token.get("user_id") or decoded_token.get("sub")
user_email = decoded_token.get("email")
token_scopes = decoded_token.get("scopes", ["read", "search"])
session_id = decoded_token.get("sid", "jwt_session")
logger.info(f"JWT token claims - user_id: {user_id}, email: {user_email}, scopes: {token_scopes}")
if user_id and user_email:
# JWT token is signed by Clerk and contains valid user info
request.state.user_id = user_id
request.state.user_email = user_email
request.state.session_id = session_id
request.state.token_scopes = token_scopes
logger.info(f"Real JWT token accepted for user: {user_id}")
else:
logger.error(f"Missing required fields in JWT token - user_id: {bool(user_id)}, email: {bool(user_email)}")
raise HTTPException(
status_code=401,
detail="Invalid token - missing user_id or email in claims"
)
except Exception as e:
logger.error(f"JWT token decoding failed: {e}")
raise HTTPException(
status_code=401,
detail="Invalid JWT token format"
)
else:
# Invalid token format - doesn't start with expected patterns
logger.error(f"Invalid token format: {token[:30]}...")
raise HTTPException(
status_code=401,
detail="Invalid token format - must be a valid JWT token"
)
except HTTPException:
# Re-raise HTTPException as-is
raise
except Exception as e:
logger.error(f"Bearer token validation failed: {str(e)}")
raise HTTPException(
status_code=401,
detail=f"Token validation failed: {str(e)}"
)
# Forward the request to the mounted MCP app
async def receive():
return await request.receive()
# Create new scope for the mounted app
scope = request.scope.copy()
scope["path"] = "/" # Root path for mounted app
scope["path_info"] = "/"
# Capture the response
response_parts = {"status": 200, "headers": [], "body": b""}
async def send(message):
if message["type"] == "http.response.start":
response_parts["status"] = message["status"]
response_parts["headers"] = message["headers"]
elif message["type"] == "http.response.body":
response_parts["body"] += message.get("body", b"")
# Call the mounted MCP app
await mcp_app(scope, receive, send)
# Return the response
from starlette.responses import Response
# Convert ASGI headers to dict
headers = {}
for name, value in response_parts["headers"]:
headers[name.decode()] = value.decode()
return Response(
content=response_parts["body"],
status_code=response_parts["status"],
headers=headers
)
# SSE transport deprecated - removed
# FastAPI health check endpoint
# FastAPI health check endpoint - BEFORE mounting MCP app
@app.get("/health")
async def health_check():
"""Health check endpoint for monitoring"""
@@ -253,6 +165,22 @@ async def health_check():
"auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
})
# Add explicit redirect for /mcp to /mcp/ with method preservation
@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"])
async def redirect_to_slash(request: Request):
"""Redirect /mcp to /mcp/ preserving HTTP method with 308"""
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/mcp/", status_code=308)
# Mount MCP app at /mcp/ with trailing slash
app.mount("/mcp/", mcp_app)
# Set the lifespan context after mounting
app.router.lifespan_context = mcp_app.lifespan
# SSE transport deprecated - removed
# FastAPI root endpoint
@app.get("/")
async def root():
@@ -374,48 +302,8 @@ async def oauth_authorization_server_root():
"resource_documentation": f"{BASE_URL}/mcp"
})
# MCP endpoint info for GET requests (ChatGPT compatibility)
@app.get("/mcp")
async def mcp_info():
"""MCP endpoint information for discovery"""
return JSONResponse({
"mcp_server": True,
"name": "Yargı MCP Server",
"version": "0.1.0",
"description": "MCP server for Turkish legal databases",
"protocol": "mcp/1.0",
"transport": ["http"],
"authentication_required": True,
"authentication": {
"type": "oauth2",
"authorization_url": "https://yargimcp.com/sign-in?redirect_url=https://api.yargimcp.com/auth/mcp-callback",
"token_url": f"{BASE_URL}/auth/mcp-token",
"scopes": ["read", "search"],
"provider": "clerk"
},
"endpoints": {
"mcp_protocol": "/mcp",
"discovery": "/mcp/discovery",
"well_known": "/.well-known/mcp",
"health": "/health",
"oauth_login": "/auth/login"
},
"capabilities": {
"tools": True,
"resources": True,
"prompts": False
},
"tools_count": len(mcp_server._tool_manager._tools),
"usage": {
"note": "This is an MCP server. Use POST to /mcp/ with proper MCP protocol headers.",
"headers_required": [
"Content-Type: application/json",
"Accept: application/json",
"Authorization: Bearer <token>",
"X-Session-ID: <session-id>"
]
}
})
# Note: GET /mcp is handled by the mounted MCP app itself
# This prevents 405 Method Not Allowed errors on POST requests
# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
@app.get("/.well-known/oauth-protected-resource")
+1 -1
View File
@@ -21,7 +21,7 @@ class BedestenSearchData(BaseModel):
pageSize: int = Field(..., description="Results per page (1-10)")
pageNumber: int = Field(..., description="Page number (1-indexed)")
itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
phrase: str = Field(..., description="Search phrase (use \"exact phrase\" for precise matching)")
phrase: str = Field(..., description="Search phrase. Supports: 'word', \"exact phrase\", +required, -exclude, AND/OR/NOT operators. No wildcards or regex.")
birimAdi: BirimAdiEnum = Field("ALL", description="""
Chamber filter (optional). Abbreviated values with Turkish names:
• Yargıtay: H1-H23 (1-23. Hukuk Dairesi), C1-C23 (1-23. Ceza Dairesi), HGK (Hukuk Genel Kurulu), CGK (Ceza Genel Kurulu), BGK (Büyük Genel Kurulu), HBK (Hukuk Daireleri Başkanlar Kurulu), CBK (Ceza Daireleri Başkanlar Kurulu)
+2 -2
View File
@@ -17,9 +17,9 @@ LOG_LEVEL = "info"
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = 'stop'
auto_stop_machines = 'off'
auto_start_machines = true
min_machines_running = 0
min_machines_running = 1
processes = ['app']
[[vm]]
+55 -16
View File
@@ -12,6 +12,13 @@ from typing import Optional, Dict, List, Literal, Any, Union
import urllib.parse
import tiktoken
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.server.dependencies import get_access_token, AccessToken
from fastmcp import Context
# Use standard exception for tool errors
class ToolError(Exception):
"""Tool execution error"""
pass
# --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
@@ -231,9 +238,25 @@ class TokenCountingMiddleware(Middleware):
# Create FastMCP app directly without authentication wrapper
from fastmcp import FastMCP
def create_app():
"""Create basic FastMCP app without authentication wrapper"""
return FastMCP("Yargı MCP Server")
def create_app(auth=None):
"""Create FastMCP app with standard capabilities and optional auth."""
global app
if auth:
# Set auth on existing app instead of creating new one
app.auth = auth
app.name = "Yargı MCP Server"
logger.info("MCP server created with Bearer authentication enabled")
else:
# Update placeholder app name only
app.name = "Yargı MCP Server"
logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)")
# Add token counting middleware
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
return app
# --- Module Imports ---
from yargitay_mcp_module.client import YargitayOfficialApiClient
@@ -322,12 +345,11 @@ from bddk_mcp_module.models import (
)
app = create_app()
# Create a placeholder app that will be properly initialized after tools are defined
from fastmcp import FastMCP
# --- Add Token Counting Middleware ---
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
# Placeholder app for decorators - will be replaced in create_app() after all tools are defined
app = FastMCP("Yargı MCP Server Placeholder")
# --- Tool Documentation Resources ---
@app.resource("docs://tools/yargitay")
@@ -1773,17 +1795,16 @@ async def get_rekabet_kurumu_document(
}
)
async def search_bedesten_unified(
phrase: str = Field(..., description="""Search query in Turkish. WORKING EXAMPLES:
ctx: Context,
phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS:
• Simple: "mülkiyet hakkı" (finds both words)
• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)
• Required term: "+mülkiyet hakkı" (must contain mülkiyet)
• Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira)
Wildcard: "mülk*" (mülkiyet, mülk, etc.)
Fuzzy: "mülkiyet~" (similar spelling variations)
Multiple terms: "mülkiyet AND hak" (both terms required)
• Either term: "mülkiyet OR tapu" (either term acceptable)
• Proximity: "\"mülkiyet hakkı\"~5" (words within 5 positions)
• Regex: "/mülk.*/" (pattern matching)
Boolean AND: "mülkiyet AND hak" (both terms required)
Boolean OR: "mülkiyet OR tapu" (either term acceptable)
Boolean NOT: "mülkiyet NOT satış" (contains mülkiyet but not satış)
NOTE: Wildcards (*,?), regex patterns (/regex/), fuzzy search (~), and proximity search are NOT supported.
For best results, use exact phrases with quotes for legal terms."""),
court_types: List[BedestenCourtTypeEnum] = Field(
default=["YARGITAYKARARI", "DANISTAYKARAR"],
@@ -1801,6 +1822,24 @@ 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
if "yargi.read" not in user_scopes and "yargi.search" not in user_scopes:
raise ToolError(f"Insufficient permissions: 'yargi.read' or 'yargi.search' scope required. Current scopes: {user_scopes}")
logger.info(f"Tool 'search_bedesten_unified' called by user '{user_id}' with scopes {user_scopes}")
except Exception as e:
# Development mode fallback - allow access without strict token validation
logger.warning(f"Bearer token validation failed, using development mode: {str(e)}")
user_id = "dev-user"
user_scopes = ["yargi.read", "yargi.search"]
pageSize = 10 # Default value
search_data = BedestenSearchData(
@@ -1815,7 +1854,7 @@ For best results, use exact phrases with quotes for legal terms."""),
search_request = BedestenSearchRequest(data=search_data)
logger.info(f"Tool 'search_bedesten_unified' called: phrase='{phrase}', court_types={court_types}, birimAdi='{birimAdi}', page={pageNumber}")
logger.info(f"User '{user_id}' searching bedesten: phrase='{phrase}', court_types={court_types}, birimAdi='{birimAdi}', page={pageNumber}")
try:
response = await bedesten_client_instance.search_documents(search_request)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "yargi-mcp"
version = "0.1.5"
version = "0.1.6"
description = "MCP Server For Turkish Legal Databases"
readme = "README.md"
requires-python = ">=3.11"