fix remote mcp

This commit is contained in:
saidsurucu
2025-07-21 18:45:20 +03:00
parent 92f04fbab6
commit 2c1b8c6f9d
2 changed files with 85 additions and 47 deletions
+46 -39
View File
@@ -21,8 +21,8 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response from starlette.responses import Response
from starlette.requests import Request as StarletteRequest from starlette.requests import Request as StarletteRequest
# Import the fully configured MCP app with all tools # Import the MCP app creator function
from mcp_server_main import app as mcp_server from mcp_server_main import create_app
# Import Stripe webhook router # Import Stripe webhook router
from stripe_webhook import router as stripe_router from stripe_webhook import router as stripe_router
@@ -40,41 +40,47 @@ logger = logging.getLogger(__name__)
# Configure CORS and Auth middleware # Configure CORS and Auth middleware
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
# Custom JWT authentication middleware for MCP endpoints # Import FastMCP Bearer Auth Provider
class JWTAuthMiddleware: from fastmcp.server.auth import BearerAuthProvider
def __init__(self, app): from fastmcp.server.auth.providers.bearer import RSAKeyPair
self.app = app
async def __call__(self, scope, receive, send): # Clerk JWT configuration for Bearer token validation
if scope["type"] == "http" and scope["path"].startswith("/mcp"): CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY")
# Only apply auth to MCP endpoints CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
request = StarletteRequest(scope, receive) CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY")
auth_header = request.headers.get("authorization") # Configure Bearer token authentication
if not auth_header or not auth_header.startswith("Bearer "): bearer_auth = None
# Return 401 for missing auth if CLERK_SECRET_KEY and CLERK_ISSUER:
response = JSONResponse( # Production: Use Clerk JWKS endpoint for token validation
status_code=401, bearer_auth = BearerAuthProvider(
content={"detail": "Missing or invalid Authorization header. Bearer token required."} jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
issuer=CLERK_ISSUER,
algorithm="RS256",
audience=CLERK_PUBLISHABLE_KEY, # Use publishable key as audience
required_scopes=["yargi.read"] # Global scope requirement
)
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"]
) )
await response(scope, receive, send)
return
# Add user info to scope for downstream processing # Generate a test token for development
token = auth_header.split(" ")[1] dev_token = dev_key_pair.create_token(
if token.startswith("eyJ"): subject="dev-user",
import jwt issuer="https://dev.yargimcp.com",
try: audience="dev-mcp-server",
decoded = jwt.decode(token, options={"verify_signature": False}) scopes=["yargi.read", "yargi.search"],
scope["user"] = { expires_in_seconds=3600 * 24 # 24 hours for development
"user_id": decoded.get("user_id") or decoded.get("sub"), )
"email": decoded.get("email"), logger.info(f"Development Bearer token: {dev_token}")
"scopes": decoded.get("scopes", ["read", "search"])
}
except:
pass
await self.app(scope, receive, send)
custom_middleware = [ custom_middleware = [
Middleware( Middleware(
@@ -82,15 +88,16 @@ custom_middleware = [
allow_origins=cors_origins, allow_origins=cors_origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS", "DELETE"], allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
allow_headers=["Content-Type", "Authorization", "X-Request-ID"], allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"],
), ),
Middleware(JWTAuthMiddleware),
] ]
# Create MCP Starlette sub-application with proper middleware # Create MCP app with Bearer authentication
mcp_server = create_app(auth=bearer_auth)
# Create MCP Starlette sub-application with middleware
mcp_app = mcp_server.http_app( mcp_app = mcp_server.http_app(
path="/mcp", custom_middleware=custom_middleware
middleware=custom_middleware
) )
# Configure JSON encoder for proper Turkish character support # Configure JSON encoder for proper Turkish character support
@@ -146,7 +153,7 @@ async def custom_401_handler(request: Request, exc: HTTPException):
return response return response
# Mount MCP app directly at /mcp path # Mount MCP app using Starlette Mount (simpler approach)
app.mount("/mcp", mcp_app) app.mount("/mcp", mcp_app)
+34 -3
View File
@@ -12,6 +12,13 @@ from typing import Optional, Dict, List, Literal, Any, Union
import urllib.parse import urllib.parse
import tiktoken import tiktoken
from fastmcp.server.middleware import Middleware, MiddlewareContext 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 --- # --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
@@ -231,8 +238,12 @@ class TokenCountingMiddleware(Middleware):
# Create FastMCP app directly without authentication wrapper # Create FastMCP app directly without authentication wrapper
from fastmcp import FastMCP from fastmcp import FastMCP
def create_app(): def create_app(auth=None):
"""Create FastMCP app with standard capabilities.""" """Create FastMCP app with standard capabilities and optional auth."""
if auth:
app = FastMCP("Yargı MCP Server", auth=auth)
logger.info("MCP server created with Bearer authentication enabled")
else:
app = FastMCP("Yargı MCP Server") app = FastMCP("Yargı MCP Server")
logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)") logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)")
return app return app
@@ -324,6 +335,7 @@ from bddk_mcp_module.models import (
) )
# Create app without auth initially (auth will be added in ASGI wrapper)
app = create_app() app = create_app()
# --- Add Token Counting Middleware --- # --- Add Token Counting Middleware ---
@@ -1775,6 +1787,7 @@ async def get_rekabet_kurumu_document(
} }
) )
async def search_bedesten_unified( async def search_bedesten_unified(
ctx: Context,
phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS: phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS:
• Simple: "mülkiyet hakkı" (finds both words) • Simple: "mülkiyet hakkı" (finds both words)
• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase) • Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)
@@ -1801,6 +1814,24 @@ For best results, use exact phrases with quotes for legal terms."""),
) -> dict: ) -> dict:
"""Search Turkish legal databases via unified Bedesten API.""" """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 pageSize = 10 # Default value
search_data = BedestenSearchData( search_data = BedestenSearchData(
@@ -1815,7 +1846,7 @@ For best results, use exact phrases with quotes for legal terms."""),
search_request = BedestenSearchRequest(data=search_data) 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: try:
response = await bedesten_client_instance.search_documents(search_request) response = await bedesten_client_instance.search_documents(search_request)