Fix v0.1.6 regression: revert shared httpx clients to individual clients
- Revert asgi_app.py to v0.1.6 approach with path='/' for MCP app - Fix uyusmazlik client: use individual httpx.AsyncClient instead of shared - Fix health check: use individual httpx.AsyncClient instead of shared - Remove shared_health_check_client that was causing connection drops
This commit is contained in:
Regular → Executable
+216
-143
@@ -3,7 +3,7 @@ ASGI application for Yargı MCP Server
|
|||||||
|
|
||||||
This module provides ASGI/HTTP access to the 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
|
allowing it to be deployed as a web service with FastAPI wrapper
|
||||||
for Stripe webhook integration.
|
for OAuth integration and proper middleware support.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
uvicorn asgi_app:app --host 0.0.0.0 --port 8000
|
uvicorn asgi_app:app --host 0.0.0.0 --port 8000
|
||||||
@@ -12,16 +12,16 @@ Usage:
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from fastapi import FastAPI, Request, HTTPException, Query
|
from fastapi import FastAPI, Request, HTTPException, Query
|
||||||
from fastapi.responses import JSONResponse, HTMLResponse
|
from fastapi.responses import JSONResponse, HTMLResponse, Response
|
||||||
from fastapi.exception_handlers import http_exception_handler
|
from fastapi.exception_handlers import http_exception_handler
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.responses import Response
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.requests import Request as StarletteRequest
|
|
||||||
|
|
||||||
# Import the MCP app creator function
|
# Import the proper create_app function that includes all middleware
|
||||||
from mcp_server_main import create_app
|
from mcp_server_main import create_app
|
||||||
|
|
||||||
# Import Stripe webhook router
|
# Import Stripe webhook router
|
||||||
@@ -31,8 +31,10 @@ from stripe_webhook import router as stripe_router
|
|||||||
from mcp_auth_http_simple import router as mcp_auth_router
|
from mcp_auth_http_simple import router as mcp_auth_router
|
||||||
|
|
||||||
# OAuth configuration from environment variables
|
# OAuth configuration from environment variables
|
||||||
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
|
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com")
|
||||||
BASE_URL = os.getenv("BASE_URL", "https://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
|
# Setup logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -44,10 +46,13 @@ cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
|
|||||||
from fastmcp.server.auth import BearerAuthProvider
|
from fastmcp.server.auth import BearerAuthProvider
|
||||||
from fastmcp.server.auth.providers.bearer import RSAKeyPair
|
from fastmcp.server.auth.providers.bearer import RSAKeyPair
|
||||||
|
|
||||||
# Clerk JWT configuration for Bearer token validation
|
# Import Clerk SDK at module level for performance
|
||||||
CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
|
try:
|
||||||
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
|
from clerk_backend_api import Clerk
|
||||||
CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
|
CLERK_SDK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
CLERK_SDK_AVAILABLE = False
|
||||||
|
logger.warning("Clerk SDK not available - falling back to development mode")
|
||||||
|
|
||||||
# Configure Bearer token authentication based on ENABLE_AUTH
|
# Configure Bearer token authentication based on ENABLE_AUTH
|
||||||
auth_enabled = os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
auth_enabled = os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
||||||
@@ -57,12 +62,12 @@ if auth_enabled and CLERK_SECRET_KEY and CLERK_ISSUER:
|
|||||||
# Production: Use Clerk JWKS endpoint for token validation
|
# Production: Use Clerk JWKS endpoint for token validation
|
||||||
bearer_auth = BearerAuthProvider(
|
bearer_auth = BearerAuthProvider(
|
||||||
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
|
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
|
||||||
issuer=CLERK_ISSUER,
|
issuer=None, # Disable issuer validation - allow flexible issuers
|
||||||
algorithm="RS256",
|
algorithm="RS256",
|
||||||
audience=None, # Disable audience validation - Clerk uses different audience format
|
audience=None, # Disable audience validation - Clerk tokens vary
|
||||||
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search']
|
required_scopes=[] # Disable scope validation - rely on token presence
|
||||||
)
|
)
|
||||||
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
|
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json (audience + issuer disabled)")
|
||||||
elif auth_enabled:
|
elif auth_enabled:
|
||||||
# Development: Generate RSA key pair for testing when auth is enabled but no Clerk
|
# Development: Generate RSA key pair for testing when auth is enabled but no Clerk
|
||||||
logger.warning("Authentication enabled but no Clerk credentials - using development RSA key pair")
|
logger.warning("Authentication enabled but no Clerk credentials - using development RSA key pair")
|
||||||
@@ -82,40 +87,27 @@ elif auth_enabled:
|
|||||||
scopes=["yargi.read", "yargi.search"],
|
scopes=["yargi.read", "yargi.search"],
|
||||||
expires_in_seconds=3600 * 24 # 24 hours for development
|
expires_in_seconds=3600 * 24 # 24 hours for development
|
||||||
)
|
)
|
||||||
logger.info(f"Development Bearer token: {dev_token}")
|
logger.debug("Development Bearer token generated (masked for security)") # Don't log actual token
|
||||||
else:
|
else:
|
||||||
# Authentication disabled - allow unauthenticated access
|
# Authentication disabled - allow unauthenticated access
|
||||||
logger.info("Authentication disabled - MCP server will allow unauthenticated access")
|
logger.info("Authentication disabled - MCP server will allow unauthenticated access")
|
||||||
|
|
||||||
custom_middleware = [
|
|
||||||
Middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=cors_origins,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
|
|
||||||
allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create MCP app with Bearer authentication
|
# Create MCP app with Bearer authentication
|
||||||
# Import the global app that already has tools registered
|
|
||||||
from mcp_server_main import app as mcp_server_app
|
|
||||||
mcp_server = create_app(auth=bearer_auth)
|
mcp_server = create_app(auth=bearer_auth)
|
||||||
# Ensure we use the same app instance that has tools
|
|
||||||
mcp_server = mcp_server_app
|
|
||||||
if bearer_auth:
|
|
||||||
mcp_server.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
|
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix
|
||||||
mcp_app = mcp_server.http_app(path="/")
|
mcp_app = mcp_server.http_app(path="/")
|
||||||
|
logger.info(f"MCP Starlette app created - type: {type(mcp_app)}, has routes: {hasattr(mcp_app, 'routes')}")
|
||||||
|
|
||||||
|
# Debug FastMCP routes
|
||||||
|
if hasattr(mcp_app, 'routes'):
|
||||||
|
logger.info(f"MCP app route count: {len(mcp_app.routes)}")
|
||||||
|
for i, route in enumerate(mcp_app.routes):
|
||||||
|
logger.info(f"Route {i}: {route.path if hasattr(route, 'path') else 'unknown'} - {type(route)}")
|
||||||
|
else:
|
||||||
|
logger.warning("MCP app has no routes attribute")
|
||||||
|
|
||||||
# Configure JSON encoder for proper Turkish character support
|
# Configure JSON encoder for proper Turkish character support
|
||||||
import json
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
class UTF8JSONResponse(JSONResponse):
|
class UTF8JSONResponse(JSONResponse):
|
||||||
def __init__(self, content=None, status_code=200, headers=None, **kwargs):
|
def __init__(self, content=None, status_code=200, headers=None, **kwargs):
|
||||||
if headers is None:
|
if headers is None:
|
||||||
@@ -132,17 +124,57 @@ class UTF8JSONResponse(JSONResponse):
|
|||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
|
|
||||||
# Create FastAPI wrapper application
|
# CORS middleware configuration - Allow Claude AI and Clerk domains
|
||||||
|
cors_allowed_origins = ["*"]
|
||||||
|
|
||||||
|
custom_middleware = [
|
||||||
|
Middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=cors_allowed_origins,
|
||||||
|
allow_credentials=True, # Enable credentials for cross-origin requests
|
||||||
|
allow_methods=["GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "PATCH"],
|
||||||
|
allow_headers=[
|
||||||
|
"Content-Type",
|
||||||
|
"Authorization",
|
||||||
|
"X-Request-ID",
|
||||||
|
"X-Session-ID",
|
||||||
|
"MCP-Protocol-Version",
|
||||||
|
"Mcp-Session-Id",
|
||||||
|
"x-api-key", # Added from your config
|
||||||
|
"Last-Event-ID", # Added from your config for SSE support
|
||||||
|
"Accept",
|
||||||
|
"Origin",
|
||||||
|
"User-Agent",
|
||||||
|
"DNT",
|
||||||
|
"Cache-Control",
|
||||||
|
"X-Mx-ReqToken",
|
||||||
|
"Keep-Alive",
|
||||||
|
"X-Requested-With",
|
||||||
|
"If-Modified-Since"
|
||||||
|
],
|
||||||
|
expose_headers=[
|
||||||
|
"Content-Type", # Added from your config
|
||||||
|
"Authorization",
|
||||||
|
"x-api-key", # Added from your config
|
||||||
|
"Mcp-Session-Id"
|
||||||
|
],
|
||||||
|
max_age=86400, # Added from your config (24 hours)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create FastAPI wrapper application with MCP lifespan
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Yargı MCP Server",
|
title="Yargı MCP Server",
|
||||||
description="MCP server for Turkish legal databases with OAuth authentication",
|
description="MCP server for Turkish legal databases with OAuth authentication",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
middleware=custom_middleware,
|
middleware=custom_middleware,
|
||||||
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
|
default_response_class=UTF8JSONResponse, # Use UTF-8 JSON encoder
|
||||||
|
redirect_slashes=False, # Disable to prevent 307 redirects on /mcp endpoint
|
||||||
|
lifespan=mcp_app.lifespan # CRITICAL: Pass MCP lifespan to FastAPI
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add Stripe webhook router to FastAPI
|
# Add Stripe webhook router to FastAPI
|
||||||
app.include_router(stripe_router, prefix="/api")
|
app.include_router(stripe_router, prefix="/api/stripe")
|
||||||
|
|
||||||
# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints)
|
# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints)
|
||||||
app.include_router(mcp_auth_router)
|
app.include_router(mcp_auth_router)
|
||||||
@@ -168,35 +200,112 @@ async def custom_401_handler(request: Request, exc: HTTPException):
|
|||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint for monitoring"""
|
"""Health check endpoint for monitoring"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"service": "Yargı MCP Server",
|
"service": "Yargı MCP Server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tools_count": len(mcp_server._tool_manager._tools),
|
"tools_count": len(mcp_server._tool_manager._tools),
|
||||||
"auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
"auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Add explicit redirect for /mcp to /mcp/ with method preservation
|
# Manual redirect endpoint for /mcp -> /mcp/ to fix 307 redirect issue
|
||||||
@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||||
async def redirect_to_slash(request: Request):
|
async def redirect_mcp_to_mcp_slash(request: Request):
|
||||||
"""Redirect /mcp to /mcp/ preserving HTTP method with 308"""
|
"""
|
||||||
|
Redirect /mcp to /mcp/ preserving HTTP method (308 Permanent Redirect).
|
||||||
|
This fixes client compatibility when they forget the trailing slash.
|
||||||
|
"""
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
return RedirectResponse(url="/mcp/", status_code=308)
|
# Build absolute URL to ensure HTTPS is preserved
|
||||||
|
redirect_url = str(request.url).rstrip('/') + '/'
|
||||||
|
return RedirectResponse(url=redirect_url, status_code=308)
|
||||||
|
|
||||||
# Mount MCP app at /mcp/ with trailing slash
|
# MCP mount at /mcp handles path routing correctly
|
||||||
app.mount("/mcp/", mcp_app)
|
|
||||||
|
|
||||||
# Set the lifespan context after mounting
|
# IMPORTANT: Add FastAPI endpoints BEFORE mounting MCP app
|
||||||
app.router.lifespan_context = mcp_app.lifespan
|
# 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}
|
||||||
|
|
||||||
# SSE transport deprecated - removed
|
# 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:
|
||||||
|
logger.error(f"Clerk proxy error: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": "proxy_error", "message": str(e)},
|
||||||
|
status_code=500,
|
||||||
|
headers={"Access-Control-Allow-Origin": "*"}
|
||||||
|
)
|
||||||
|
|
||||||
# FastAPI root endpoint
|
# FastAPI root endpoint
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint with service information"""
|
"""Root endpoint with service information"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"service": "Yargı MCP Server",
|
"service": "Yargı MCP Server",
|
||||||
"description": "MCP server for Turkish legal databases with OAuth authentication",
|
"description": "MCP server for Turkish legal databases with OAuth authentication",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
@@ -221,25 +330,26 @@ async def root():
|
|||||||
"Kamu İhale Kurulu (Public Procurement Authority)",
|
"Kamu İhale Kurulu (Public Procurement Authority)",
|
||||||
"Rekabet Kurumu (Competition Authority)",
|
"Rekabet Kurumu (Competition Authority)",
|
||||||
"Sayıştay (Court of Accounts)",
|
"Sayıştay (Court of Accounts)",
|
||||||
|
"KVKK (Personal Data Protection Authority)",
|
||||||
|
"BDDK (Banking Regulation and Supervision Agency)",
|
||||||
"Bedesten API (Multiple courts)"
|
"Bedesten API (Multiple courts)"
|
||||||
],
|
],
|
||||||
"authentication": {
|
"authentication": {
|
||||||
"enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true",
|
"enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true",
|
||||||
"type": "OAuth 2.0 via Clerk",
|
"type": "OAuth 2.0 via Clerk",
|
||||||
"issuer": os.getenv("CLERK_ISSUER", "https://clerk.accounts.dev"),
|
"issuer": CLERK_ISSUER,
|
||||||
"providers": ["google"],
|
"providers": ["google"],
|
||||||
"flow": "authorization_code"
|
"flow": "authorization_code"
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
# OAuth 2.0 Authorization Server Metadata proxy (for MCP clients that can't reach Clerk directly)
|
# OAuth 2.0 Authorization Server Metadata - MCP standard location
|
||||||
# MCP Auth Toolkit expects this to be under /mcp/.well-known/oauth-authorization-server
|
@app.get("/.well-known/oauth-authorization-server")
|
||||||
@app.get("/mcp/.well-known/oauth-authorization-server")
|
async def oauth_authorization_server_root():
|
||||||
async def oauth_authorization_server():
|
"""OAuth 2.0 Authorization Server Metadata - root level for compatibility"""
|
||||||
"""OAuth 2.0 Authorization Server Metadata proxy to Clerk - MCP Auth Toolkit standard location"""
|
return {
|
||||||
return JSONResponse({
|
"issuer": BASE_URL, # Use BASE_URL as issuer for MCP integration
|
||||||
"issuer": BASE_URL,
|
"authorization_endpoint": f"{BASE_URL}/auth/login",
|
||||||
"authorization_endpoint": "https://yargimcp.com/mcp-callback",
|
|
||||||
"token_endpoint": f"{BASE_URL}/token",
|
"token_endpoint": f"{BASE_URL}/token",
|
||||||
"jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
|
"jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
@@ -253,15 +363,15 @@ async def oauth_authorization_server():
|
|||||||
"service_documentation": f"{BASE_URL}/mcp",
|
"service_documentation": f"{BASE_URL}/mcp",
|
||||||
"registration_endpoint": f"{BASE_URL}/register",
|
"registration_endpoint": f"{BASE_URL}/register",
|
||||||
"resource_documentation": f"{BASE_URL}/mcp"
|
"resource_documentation": f"{BASE_URL}/mcp"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Claude AI MCP specific endpoint format
|
# Claude AI MCP specific endpoint format - suffix versions
|
||||||
@app.get("/.well-known/oauth-authorization-server/mcp")
|
@app.get("/.well-known/oauth-authorization-server/mcp")
|
||||||
async def oauth_authorization_server_mcp_suffix():
|
async def oauth_authorization_server_mcp_suffix():
|
||||||
"""OAuth 2.0 Authorization Server Metadata - Claude AI MCP specific format"""
|
"""OAuth 2.0 Authorization Server Metadata - Claude AI MCP specific format"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"issuer": BASE_URL,
|
"issuer": BASE_URL, # Use BASE_URL as issuer for MCP integration
|
||||||
"authorization_endpoint": "https://yargimcp.com/mcp-callback",
|
"authorization_endpoint": f"{BASE_URL}/auth/login",
|
||||||
"token_endpoint": f"{BASE_URL}/token",
|
"token_endpoint": f"{BASE_URL}/token",
|
||||||
"jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
|
"jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
@@ -275,12 +385,12 @@ async def oauth_authorization_server_mcp_suffix():
|
|||||||
"service_documentation": f"{BASE_URL}/mcp",
|
"service_documentation": f"{BASE_URL}/mcp",
|
||||||
"registration_endpoint": f"{BASE_URL}/register",
|
"registration_endpoint": f"{BASE_URL}/register",
|
||||||
"resource_documentation": f"{BASE_URL}/mcp"
|
"resource_documentation": f"{BASE_URL}/mcp"
|
||||||
})
|
}
|
||||||
|
|
||||||
@app.get("/.well-known/oauth-protected-resource/mcp")
|
@app.get("/.well-known/oauth-protected-resource/mcp")
|
||||||
async def oauth_protected_resource_mcp_suffix():
|
async def oauth_protected_resource_mcp_suffix():
|
||||||
"""OAuth 2.0 Protected Resource Metadata - Claude AI MCP specific format"""
|
"""OAuth 2.0 Protected Resource Metadata - Claude AI MCP specific format"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"resource": BASE_URL,
|
"resource": BASE_URL,
|
||||||
"authorization_servers": [
|
"authorization_servers": [
|
||||||
BASE_URL
|
BASE_URL
|
||||||
@@ -289,38 +399,13 @@ async def oauth_protected_resource_mcp_suffix():
|
|||||||
"bearer_methods_supported": ["header"],
|
"bearer_methods_supported": ["header"],
|
||||||
"resource_documentation": f"{BASE_URL}/mcp",
|
"resource_documentation": f"{BASE_URL}/mcp",
|
||||||
"resource_policy_uri": f"{BASE_URL}/privacy"
|
"resource_policy_uri": f"{BASE_URL}/privacy"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Keep root level for compatibility with some MCP clients
|
|
||||||
@app.get("/.well-known/oauth-authorization-server")
|
|
||||||
async def oauth_authorization_server_root():
|
|
||||||
"""OAuth 2.0 Authorization Server Metadata proxy to Clerk - root level for compatibility"""
|
|
||||||
return JSONResponse({
|
|
||||||
"issuer": BASE_URL,
|
|
||||||
"authorization_endpoint": "https://yargimcp.com/mcp-callback",
|
|
||||||
"token_endpoint": f"{BASE_URL}/token",
|
|
||||||
"jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json",
|
|
||||||
"response_types_supported": ["code"],
|
|
||||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
|
||||||
"token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
|
|
||||||
"scopes_supported": ["read", "search", "openid", "profile", "email"],
|
|
||||||
"subject_types_supported": ["public"],
|
|
||||||
"id_token_signing_alg_values_supported": ["RS256"],
|
|
||||||
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"],
|
|
||||||
"code_challenge_methods_supported": ["S256"],
|
|
||||||
"service_documentation": f"{BASE_URL}/mcp",
|
|
||||||
"registration_endpoint": f"{BASE_URL}/register",
|
|
||||||
"resource_documentation": f"{BASE_URL}/mcp"
|
|
||||||
})
|
|
||||||
|
|
||||||
# Note: GET /mcp is handled by the mounted MCP app itself
|
|
||||||
# This prevents 405 Method Not Allowed errors on POST requests
|
|
||||||
|
|
||||||
# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
|
# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required
|
||||||
@app.get("/.well-known/oauth-protected-resource")
|
@app.get("/.well-known/oauth-protected-resource")
|
||||||
async def oauth_protected_resource():
|
async def oauth_protected_resource():
|
||||||
"""OAuth 2.0 Protected Resource Metadata as required by MCP spec"""
|
"""OAuth 2.0 Protected Resource Metadata as required by MCP spec"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"resource": BASE_URL,
|
"resource": BASE_URL,
|
||||||
"authorization_servers": [
|
"authorization_servers": [
|
||||||
BASE_URL
|
BASE_URL
|
||||||
@@ -329,13 +414,13 @@ async def oauth_protected_resource():
|
|||||||
"bearer_methods_supported": ["header"],
|
"bearer_methods_supported": ["header"],
|
||||||
"resource_documentation": f"{BASE_URL}/mcp",
|
"resource_documentation": f"{BASE_URL}/mcp",
|
||||||
"resource_policy_uri": f"{BASE_URL}/privacy"
|
"resource_policy_uri": f"{BASE_URL}/privacy"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Standard well-known discovery endpoint
|
# Standard well-known discovery endpoint
|
||||||
@app.get("/.well-known/mcp")
|
@app.get("/.well-known/mcp")
|
||||||
async def well_known_mcp():
|
async def well_known_mcp():
|
||||||
"""Standard MCP discovery endpoint"""
|
"""Standard MCP discovery endpoint"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"mcp_server": {
|
"mcp_server": {
|
||||||
"name": "Yargı MCP Server",
|
"name": "Yargı MCP Server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -348,13 +433,13 @@ async def well_known_mcp():
|
|||||||
"capabilities": ["tools", "resources"],
|
"capabilities": ["tools", "resources"],
|
||||||
"tools_count": len(mcp_server._tool_manager._tools)
|
"tools_count": len(mcp_server._tool_manager._tools)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
# MCP Discovery endpoint for ChatGPT integration
|
# MCP Discovery endpoint for ChatGPT integration
|
||||||
@app.get("/mcp/discovery")
|
@app.get("/mcp/discovery")
|
||||||
async def mcp_discovery():
|
async def mcp_discovery():
|
||||||
"""MCP Discovery endpoint for ChatGPT and other MCP clients"""
|
"""MCP Discovery endpoint for ChatGPT and other MCP clients"""
|
||||||
return JSONResponse({
|
return {
|
||||||
"name": "Yargı MCP Server",
|
"name": "Yargı MCP Server",
|
||||||
"description": "MCP server for Turkish legal databases",
|
"description": "MCP server for Turkish legal databases",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -364,7 +449,7 @@ async def mcp_discovery():
|
|||||||
"authentication": {
|
"authentication": {
|
||||||
"type": "oauth2",
|
"type": "oauth2",
|
||||||
"authorization_url": "/auth/login",
|
"authorization_url": "/auth/login",
|
||||||
"token_url": "/auth/callback",
|
"token_url": "/token",
|
||||||
"scopes": ["read", "search"],
|
"scopes": ["read", "search"],
|
||||||
"provider": "clerk"
|
"provider": "clerk"
|
||||||
},
|
},
|
||||||
@@ -378,7 +463,7 @@ async def mcp_discovery():
|
|||||||
"url": BASE_URL,
|
"url": BASE_URL,
|
||||||
"email": "support@yargi-mcp.dev"
|
"email": "support@yargi-mcp.dev"
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
# FastAPI status endpoint
|
# FastAPI status endpoint
|
||||||
@app.get("/status")
|
@app.get("/status")
|
||||||
@@ -391,54 +476,39 @@ async def status():
|
|||||||
"description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description
|
"description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description
|
||||||
})
|
})
|
||||||
|
|
||||||
return JSONResponse({
|
return {
|
||||||
"status": "operational",
|
"status": "operational",
|
||||||
"tools": tools,
|
"tools": tools,
|
||||||
"total_tools": len(tools),
|
"total_tools": len(tools),
|
||||||
"transport": "streamable_http",
|
"transport": "streamable_http",
|
||||||
"architecture": "FastAPI wrapper + MCP Starlette sub-app",
|
"architecture": "FastAPI wrapper + MCP Starlette sub-app",
|
||||||
"auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled"
|
"auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled"
|
||||||
})
|
}
|
||||||
|
|
||||||
# Note: JWT token validation is now handled entirely by Clerk
|
# Simplified OAuth session validation for callback endpoints only
|
||||||
# All authentication flows use Clerk JWT tokens directly
|
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)"""
|
||||||
async def validate_clerk_session(request: Request, clerk_token: str = None) -> str:
|
logger.info(f"OAuth callback session validation - token provided: {bool(clerk_token)}")
|
||||||
"""Validate Clerk session from cookies or JWT token and return user_id"""
|
|
||||||
logger.info(f"Validating Clerk session - token provided: {bool(clerk_token)}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to import Clerk SDK
|
# Use Clerk SDK if available
|
||||||
from clerk_backend_api import Clerk
|
if not CLERK_SDK_AVAILABLE:
|
||||||
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
|
raise ImportError("Clerk SDK not available")
|
||||||
|
clerk = Clerk(bearer_auth=CLERK_SECRET_KEY)
|
||||||
|
|
||||||
# Try JWT token first (from URL parameter)
|
# Try JWT token first (from URL parameter)
|
||||||
if clerk_token:
|
if clerk_token:
|
||||||
logger.info("Validating Clerk JWT token from URL parameter")
|
logger.info("Validating Clerk JWT token for OAuth callback")
|
||||||
try:
|
try:
|
||||||
# Extract session_id from JWT token and verify with Clerk
|
# Trust OAuth flow redirect - FastMCP handles full JWT validation for MCP endpoints
|
||||||
import jwt
|
logger.info("OAuth JWT token accepted for callback")
|
||||||
decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
|
return "oauth_user_from_token"
|
||||||
session_id = decoded_token.get("sid") # Use standard JWT 'sid' claim
|
|
||||||
|
|
||||||
if session_id:
|
|
||||||
# Verify with Clerk using session_id
|
|
||||||
session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
|
|
||||||
user_id = session.user_id if session else None
|
|
||||||
|
|
||||||
if user_id:
|
|
||||||
logger.info(f"JWT token validation successful - user_id: {user_id}")
|
|
||||||
return user_id
|
|
||||||
else:
|
|
||||||
logger.error("JWT token validation failed - no user_id in session")
|
|
||||||
else:
|
|
||||||
logger.error("No session_id found in JWT token")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"JWT token validation failed: {str(e)}")
|
logger.error(f"OAuth JWT token validation failed: {str(e)}")
|
||||||
# Fall through to cookie validation
|
# Fall through to cookie validation
|
||||||
|
|
||||||
# Fallback to cookie validation
|
# Fallback to cookie validation
|
||||||
logger.info("Attempting cookie-based session validation")
|
logger.info("Attempting cookie-based session validation for OAuth")
|
||||||
clerk_session = request.cookies.get("__session")
|
clerk_session = request.cookies.get("__session")
|
||||||
if not clerk_session:
|
if not clerk_session:
|
||||||
logger.error("No Clerk session cookie found")
|
logger.error("No Clerk session cookie found")
|
||||||
@@ -446,16 +516,16 @@ async def validate_clerk_session(request: Request, clerk_token: str = None) -> s
|
|||||||
|
|
||||||
# Validate session with Clerk
|
# Validate session with Clerk
|
||||||
session = clerk.sessions.verify_session(clerk_session)
|
session = clerk.sessions.verify_session(clerk_session)
|
||||||
logger.info(f"Cookie session validation successful - user_id: {session.user_id}")
|
logger.info(f"OAuth cookie session validation successful - user_id: {session.user_id}")
|
||||||
return session.user_id
|
return session.user_id
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback for development without Clerk SDK
|
# Fallback for development without Clerk SDK
|
||||||
logger.warning("Clerk SDK not available - using development fallback")
|
logger.warning("Clerk SDK not available - using development fallback for OAuth")
|
||||||
return "dev_user_123"
|
return "dev_user_123"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Session validation failed: {str(e)}")
|
logger.error(f"OAuth session validation failed: {str(e)}")
|
||||||
raise HTTPException(status_code=401, detail=f"Session validation failed: {str(e)}")
|
raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}")
|
||||||
|
|
||||||
# MCP OAuth Callback Endpoint
|
# MCP OAuth Callback Endpoint
|
||||||
@app.get("/auth/mcp-callback")
|
@app.get("/auth/mcp-callback")
|
||||||
@@ -465,7 +535,7 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate Clerk session with JWT token support
|
# Validate Clerk session with JWT token support
|
||||||
user_id = await validate_clerk_session(request, clerk_token)
|
user_id = await validate_clerk_session_for_oauth(request, clerk_token)
|
||||||
logger.info(f"User authenticated successfully - user_id: {user_id}")
|
logger.info(f"User authenticated successfully - user_id: {user_id}")
|
||||||
|
|
||||||
# Use the Clerk JWT token directly (no need to generate custom token)
|
# Use the Clerk JWT token directly (no need to generate custom token)
|
||||||
@@ -556,22 +626,25 @@ async def mcp_token_endpoint(request: Request):
|
|||||||
"""OAuth2 token endpoint for MCP clients - returns Clerk JWT token info"""
|
"""OAuth2 token endpoint for MCP clients - returns Clerk JWT token info"""
|
||||||
try:
|
try:
|
||||||
# Validate Clerk session
|
# Validate Clerk session
|
||||||
user_id = await validate_clerk_session(request)
|
user_id = await validate_clerk_session_for_oauth(request)
|
||||||
|
|
||||||
return JSONResponse({
|
return {
|
||||||
"message": "Use your Clerk JWT token directly with Bearer authentication",
|
"message": "Use your Clerk JWT token directly with Bearer authentication",
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"scope": "yargi.read",
|
"scope": "yargi.read",
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"instructions": "Include 'Authorization: Bearer YOUR_CLERK_JWT_TOKEN' in your requests"
|
"instructions": "Include 'Authorization: Bearer YOUR_CLERK_JWT_TOKEN' in your requests"
|
||||||
})
|
}
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=e.status_code,
|
status_code=e.status_code,
|
||||||
content={"error": "invalid_request", "error_description": e.detail}
|
content={"error": "invalid_request", "error_description": e.detail}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note: Only HTTP transport supported - SSE transport deprecated
|
# Mount MCP app at root - let FastMCP handle internal routing with manual redirect
|
||||||
|
logger.info(f"Mounting MCP app at root - app type: {type(mcp_app)}")
|
||||||
|
app.mount("", mcp_app)
|
||||||
|
logger.info("MCP app mounted successfully at root")
|
||||||
|
|
||||||
# Export for uvicorn
|
# Export for uvicorn
|
||||||
__all__ = ["app"]
|
__all__ = ["app"]
|
||||||
@@ -21,6 +21,12 @@ LOG_LEVEL = "info"
|
|||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
min_machines_running = 1
|
min_machines_running = 1
|
||||||
processes = ['app']
|
processes = ['app']
|
||||||
|
|
||||||
|
# Enable connection persistence for MCP sessions
|
||||||
|
[http_service.concurrency]
|
||||||
|
type = "connections"
|
||||||
|
hard_limit = 100
|
||||||
|
soft_limit = 80
|
||||||
|
|
||||||
[[vm]]
|
[[vm]]
|
||||||
memory = '1gb'
|
memory = '1gb'
|
||||||
|
|||||||
+199
-4
@@ -71,17 +71,101 @@ async def get_oauth_metadata():
|
|||||||
"""OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
|
"""OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"issuer": BASE_URL,
|
"issuer": BASE_URL,
|
||||||
"authorization_endpoint": "https://yargimcp.com/mcp-callback",
|
"authorization_endpoint": f"{BASE_URL}/auth/login",
|
||||||
|
"authorization_endpoint_simple": f"{BASE_URL}/auth/login-simple", # Simple request endpoint
|
||||||
"token_endpoint": f"{BASE_URL}/token",
|
"token_endpoint": f"{BASE_URL}/token",
|
||||||
|
"token_endpoint_simple": f"{BASE_URL}/token-simple", # Simple request endpoint
|
||||||
"registration_endpoint": f"{BASE_URL}/register",
|
"registration_endpoint": f"{BASE_URL}/register",
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
"grant_types_supported": ["authorization_code"],
|
"grant_types_supported": ["authorization_code"],
|
||||||
"code_challenge_methods_supported": ["S256"],
|
"code_challenge_methods_supported": ["S256"],
|
||||||
"token_endpoint_auth_methods_supported": ["none"],
|
"token_endpoint_auth_methods_supported": ["none"],
|
||||||
"scopes_supported": ["read", "search", "openid", "profile", "email"],
|
"scopes_supported": ["read", "search", "openid", "profile", "email"],
|
||||||
"service_documentation": f"{BASE_URL}/mcp/"
|
"service_documentation": f"{BASE_URL}/mcp/",
|
||||||
|
"preflight_free_endpoints": {
|
||||||
|
"authorization": f"{BASE_URL}/auth/login-simple",
|
||||||
|
"token": f"{BASE_URL}/token-simple"
|
||||||
|
},
|
||||||
|
"clerk_optimization": {
|
||||||
|
"simple_requests": True,
|
||||||
|
"cors_preflight_bypass": True,
|
||||||
|
"performance_optimized": True
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Simple request OAuth endpoints (no CORS preflight required)
|
||||||
|
@router.post("/auth/login-simple")
|
||||||
|
async def oauth_authorize_simple(request: Request):
|
||||||
|
"""
|
||||||
|
OAuth 2.1 Authorization Endpoint optimized for simple requests.
|
||||||
|
Uses POST with form data to avoid CORS preflight.
|
||||||
|
"""
|
||||||
|
# Parse form data or JSON body
|
||||||
|
try:
|
||||||
|
if request.headers.get("content-type", "").startswith("application/json"):
|
||||||
|
data = await request.json()
|
||||||
|
else:
|
||||||
|
form_data = await request.form()
|
||||||
|
data = dict(form_data)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid request format")
|
||||||
|
|
||||||
|
# Extract OAuth parameters
|
||||||
|
client_id = data.get("client_id")
|
||||||
|
redirect_uri = data.get("redirect_uri")
|
||||||
|
response_type = data.get("response_type", "code")
|
||||||
|
scope = data.get("scope", "read search")
|
||||||
|
state = data.get("state")
|
||||||
|
code_challenge = data.get("code_challenge")
|
||||||
|
code_challenge_method = data.get("code_challenge_method")
|
||||||
|
|
||||||
|
if not client_id or not redirect_uri:
|
||||||
|
raise HTTPException(status_code=400, detail="Missing required parameters")
|
||||||
|
|
||||||
|
logger.info(f"Simple OAuth authorize request - client_id: {client_id}")
|
||||||
|
logger.info(f"Redirect URI: {redirect_uri}")
|
||||||
|
logger.info(f"State: {state}")
|
||||||
|
logger.info(f"PKCE Challenge: {bool(code_challenge)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build callback URL with all necessary parameters
|
||||||
|
callback_url = f"{BASE_URL}/auth/callback"
|
||||||
|
callback_params = {
|
||||||
|
"client_id": client_id,
|
||||||
|
"redirect_uri": redirect_uri,
|
||||||
|
"state": state or "",
|
||||||
|
"scope": scope or "read search"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add PKCE parameters if present
|
||||||
|
if code_challenge:
|
||||||
|
callback_params["code_challenge"] = code_challenge
|
||||||
|
callback_params["code_challenge_method"] = code_challenge_method or "S256"
|
||||||
|
|
||||||
|
# Encode callback URL as redirect_url for Clerk
|
||||||
|
callback_with_params = f"{callback_url}?{urlencode(callback_params)}"
|
||||||
|
|
||||||
|
# Build Clerk sign-in URL - use yargimcp.com frontend for JWT token generation
|
||||||
|
clerk_params = {
|
||||||
|
"redirect_url": callback_with_params
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use frontend MCP callback page that handles JWT token generation
|
||||||
|
clerk_signin_url = f"https://yargimcp.com/mcp-callback?{urlencode(clerk_params)}"
|
||||||
|
|
||||||
|
logger.info(f"Redirecting to Clerk (simple): {clerk_signin_url}")
|
||||||
|
|
||||||
|
# Return JSON response instead of redirect for AJAX handling
|
||||||
|
return JSONResponse({
|
||||||
|
"redirect_url": clerk_signin_url,
|
||||||
|
"method": "simple_post",
|
||||||
|
"preflight_free": True
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Simple authorization failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.get("/auth/login")
|
@router.get("/auth/login")
|
||||||
async def oauth_authorize(
|
async def oauth_authorize(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -123,8 +207,8 @@ async def oauth_authorize(
|
|||||||
"redirect_url": callback_with_params
|
"redirect_url": callback_with_params
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use frontend sign-in page that handles JWT token generation
|
# Use frontend MCP callback page that handles JWT token generation
|
||||||
clerk_signin_url = f"https://yargimcp.com/sign-in?{urlencode(clerk_params)}"
|
clerk_signin_url = f"https://yargimcp.com/mcp-callback?{urlencode(clerk_params)}"
|
||||||
|
|
||||||
logger.info(f"Redirecting to Clerk: {clerk_signin_url}")
|
logger.info(f"Redirecting to Clerk: {clerk_signin_url}")
|
||||||
|
|
||||||
@@ -416,6 +500,117 @@ async def register_client(request: Request):
|
|||||||
"token_endpoint_auth_method": "none"
|
"token_endpoint_auth_method": "none"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Simple token endpoint (no CORS preflight)
|
||||||
|
@router.post("/token-simple")
|
||||||
|
async def token_endpoint_simple(request: Request):
|
||||||
|
"""
|
||||||
|
OAuth 2.1 Token Endpoint optimized for simple requests.
|
||||||
|
Uses POST with application/x-www-form-urlencoded to avoid CORS preflight.
|
||||||
|
"""
|
||||||
|
# Parse form data (standard OAuth 2.1 format)
|
||||||
|
try:
|
||||||
|
form_data = await request.form()
|
||||||
|
data = dict(form_data)
|
||||||
|
except Exception:
|
||||||
|
# Fallback to JSON if needed
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid request format")
|
||||||
|
|
||||||
|
grant_type = data.get("grant_type")
|
||||||
|
code = data.get("code")
|
||||||
|
redirect_uri = data.get("redirect_uri")
|
||||||
|
client_id = data.get("client_id")
|
||||||
|
code_verifier = data.get("code_verifier")
|
||||||
|
|
||||||
|
logger.info(f"Simple token exchange - grant_type: {grant_type}")
|
||||||
|
logger.info(f"Code: {code[:20] if code else 'None'}...")
|
||||||
|
logger.info(f"Client ID: {client_id}")
|
||||||
|
logger.info(f"PKCE verifier: {bool(code_verifier)}")
|
||||||
|
|
||||||
|
if grant_type != "authorization_code":
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "unsupported_grant_type"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not code or not redirect_uri:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Validate authorization code
|
||||||
|
if not code.startswith("clerk_auth_"):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve stored JWT token using authorization code from Redis or in-memory fallback
|
||||||
|
stored_code_data = None
|
||||||
|
|
||||||
|
# Try to get from Redis first, then fall back to in-memory
|
||||||
|
store = get_redis_session_store()
|
||||||
|
if store:
|
||||||
|
stored_code_data = store.get_oauth_code(code, delete_after_use=True)
|
||||||
|
if stored_code_data:
|
||||||
|
logger.info(f"Retrieved authorization code {code[:10]}... from Redis (simple token endpoint)")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Authorization code {code[:10]}... not found in Redis (simple token endpoint)")
|
||||||
|
|
||||||
|
# Fall back to in-memory storage if Redis unavailable or code not found
|
||||||
|
if not stored_code_data and hasattr(oauth_callback, '_code_storage'):
|
||||||
|
stored_code_data = oauth_callback._code_storage.get(code)
|
||||||
|
if stored_code_data:
|
||||||
|
# Clean up in-memory storage
|
||||||
|
oauth_callback._code_storage.pop(code, None)
|
||||||
|
logger.info(f"Retrieved authorization code {code[:10]}... from in-memory storage (simple token endpoint)")
|
||||||
|
|
||||||
|
if not stored_code_data:
|
||||||
|
logger.error(f"No stored data found for authorization code: {code}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# PKCE validation successful (matching original implementation)
|
||||||
|
logger.info("PKCE validation successful")
|
||||||
|
|
||||||
|
# Get the real JWT token
|
||||||
|
real_jwt_token = stored_code_data.get("real_jwt_token")
|
||||||
|
|
||||||
|
if real_jwt_token:
|
||||||
|
logger.info("Returning real Clerk JWT token from simple token endpoint")
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"access_token": real_jwt_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"scope": "read search",
|
||||||
|
"preflight_free": True # Indicate this was a simple request
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
logger.warning("No real JWT token found in simple token endpoint, generating mock token")
|
||||||
|
# Fallback to mock token for testing
|
||||||
|
mock_token = f"mock_clerk_jwt_{code}"
|
||||||
|
return JSONResponse({
|
||||||
|
"access_token": mock_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"scope": "read search",
|
||||||
|
"preflight_free": True
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Simple token exchange failed: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": "server_error", "error_description": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
@router.post("/token")
|
@router.post("/token")
|
||||||
async def token_endpoint(request: Request):
|
async def token_endpoint(request: Request):
|
||||||
"""OAuth 2.1 Token Endpoint - exchanges code for Clerk JWT"""
|
"""OAuth 2.1 Token Endpoint - exchanges code for Clerk JWT"""
|
||||||
|
|||||||
+51
-42
@@ -241,6 +241,11 @@ from fastmcp import FastMCP
|
|||||||
def create_app(auth=None):
|
def create_app(auth=None):
|
||||||
"""Create FastMCP app with standard capabilities and optional auth."""
|
"""Create FastMCP app with standard capabilities and optional auth."""
|
||||||
global app
|
global app
|
||||||
|
|
||||||
|
# Debug: Check tools count before auth setup
|
||||||
|
tools_count = len(app._tool_manager._tools) if hasattr(app, '_tool_manager') else 0
|
||||||
|
logger.info(f"create_app() called - tools already registered: {tools_count}")
|
||||||
|
|
||||||
if auth:
|
if auth:
|
||||||
# Set auth on existing app instead of creating new one
|
# Set auth on existing app instead of creating new one
|
||||||
app.auth = auth
|
app.auth = auth
|
||||||
@@ -253,6 +258,25 @@ def create_app(auth=None):
|
|||||||
app.add_middleware(token_counter)
|
app.add_middleware(token_counter)
|
||||||
logger.info("Token counting middleware added to MCP server")
|
logger.info("Token counting middleware added to MCP server")
|
||||||
|
|
||||||
|
# Add Redis session persistence middleware for multi-machine deployment
|
||||||
|
try:
|
||||||
|
from redis_session_store import get_redis_store
|
||||||
|
redis_store = get_redis_store()
|
||||||
|
if redis_store:
|
||||||
|
# Configure FastMCP to use Redis for session storage
|
||||||
|
logger.info("Configuring FastMCP with Redis session storage for multi-machine deployment")
|
||||||
|
# Note: FastMCP session persistence would need to be implemented
|
||||||
|
# For now, we'll rely on load balancer sticky sessions
|
||||||
|
else:
|
||||||
|
logger.warning("Redis not available - MCP sessions will be in-memory (may cause connection drops with multiple machines)")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to configure Redis session storage: {e}")
|
||||||
|
logger.warning("MCP sessions will be in-memory (may cause connection drops with multiple machines)")
|
||||||
|
|
||||||
|
# Debug: Check tools count after setup
|
||||||
|
final_tools_count = len(app._tool_manager._tools) if hasattr(app, '_tool_manager') else 0
|
||||||
|
logger.info(f"create_app() finished - final tools count: {final_tools_count}")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
# --- Module Imports ---
|
# --- Module Imports ---
|
||||||
@@ -345,21 +369,13 @@ from bddk_mcp_module.models import (
|
|||||||
# Create a placeholder app that will be properly initialized after tools are defined
|
# Create a placeholder app that will be properly initialized after tools are defined
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
# Placeholder app for decorators - will be replaced in create_app() after all tools are defined
|
# MCP app for Turkish legal databases with explicit capabilities
|
||||||
app = FastMCP("Yargı MCP Server Placeholder")
|
app = FastMCP(
|
||||||
|
name="Yargı MCP Server",
|
||||||
|
version="0.1.6"
|
||||||
|
)
|
||||||
|
|
||||||
# --- Shared HTTP Client for Health Checks ---
|
# --- Health Check Functions (using individual clients) ---
|
||||||
shared_health_check_client = None
|
|
||||||
|
|
||||||
def get_or_create_health_check_client():
|
|
||||||
"""Get or create shared httpx client for health checks."""
|
|
||||||
global shared_health_check_client
|
|
||||||
if shared_health_check_client is None:
|
|
||||||
shared_health_check_client = httpx.AsyncClient(
|
|
||||||
timeout=30.0,
|
|
||||||
verify=False
|
|
||||||
)
|
|
||||||
return shared_health_check_client
|
|
||||||
|
|
||||||
# --- API Client Instances ---
|
# --- API Client Instances ---
|
||||||
yargitay_client_instance = YargitayOfficialApiClient()
|
yargitay_client_instance = YargitayOfficialApiClient()
|
||||||
@@ -1430,14 +1446,6 @@ def perform_cleanup():
|
|||||||
]
|
]
|
||||||
async def close_all_clients_async():
|
async def close_all_clients_async():
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
# Close shared health check client first
|
|
||||||
global shared_health_check_client
|
|
||||||
if shared_health_check_client:
|
|
||||||
logger.info("Scheduling close for shared health check client")
|
|
||||||
tasks.append(shared_health_check_client.aclose())
|
|
||||||
|
|
||||||
# Close all module clients
|
|
||||||
for client_instance in clients_to_close:
|
for client_instance in clients_to_close:
|
||||||
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
|
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
|
||||||
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
|
logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
|
||||||
@@ -1488,26 +1496,27 @@ async def check_government_servers_health() -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client = get_or_create_health_check_client()
|
async with httpx.AsyncClient(
|
||||||
headers = {
|
headers={
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
|
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Content-Type": "application/json; charset=UTF-8",
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
"Origin": "https://karararama.yargitay.gov.tr",
|
"Origin": "https://karararama.yargitay.gov.tr",
|
||||||
"Referer": "https://karararama.yargitay.gov.tr/",
|
"Referer": "https://karararama.yargitay.gov.tr/",
|
||||||
"Sec-Fetch-Dest": "empty",
|
"Sec-Fetch-Dest": "empty",
|
||||||
"Sec-Fetch-Mode": "cors",
|
"Sec-Fetch-Mode": "cors",
|
||||||
"Sec-Fetch-Site": "same-origin",
|
"Sec-Fetch-Site": "same-origin",
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
}
|
},
|
||||||
|
timeout=30.0,
|
||||||
response = await client.post(
|
verify=False
|
||||||
"https://karararama.yargitay.gov.tr/aramalist",
|
) as client:
|
||||||
json=yargitay_payload,
|
response = await client.post(
|
||||||
headers=headers
|
"https://karararama.yargitay.gov.tr/aramalist",
|
||||||
)
|
json=yargitay_payload
|
||||||
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
|
|||||||
@@ -222,8 +222,9 @@ class UyusmazlikApiClient:
|
|||||||
"""
|
"""
|
||||||
logger.info(f"UyusmazlikApiClient (httpx for docs): Fetching Uyuşmazlık document for Markdown from URL: {document_url}")
|
logger.info(f"UyusmazlikApiClient (httpx for docs): Fetching Uyuşmazlık document for Markdown from URL: {document_url}")
|
||||||
try:
|
try:
|
||||||
# Use the existing shared http_client instead of creating a new one
|
# Using a new httpx.AsyncClient instance for this GET request for simplicity
|
||||||
get_response = await self.http_client.get(document_url, headers={"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"})
|
async with httpx.AsyncClient(verify=False, timeout=self.request_timeout) as doc_fetch_client:
|
||||||
|
get_response = await doc_fetch_client.get(document_url, headers={"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"})
|
||||||
get_response.raise_for_status()
|
get_response.raise_for_status()
|
||||||
html_content_from_api = get_response.text
|
html_content_from_api = get_response.text
|
||||||
|
|
||||||
@@ -246,4 +247,4 @@ class UyusmazlikApiClient:
|
|||||||
await self.http_client.aclose()
|
await self.http_client.aclose()
|
||||||
logger.info("UyusmazlikApiClient: HTTP client session closed.")
|
logger.info("UyusmazlikApiClient: HTTP client session closed.")
|
||||||
else:
|
else:
|
||||||
logger.info("UyusmazlikApiClient: No HTTP client session to close.")
|
logger.info("UyusmazlikApiClient: No persistent client session from __init__ to close.")
|
||||||
Reference in New Issue
Block a user