fix remote mcp
This commit is contained in:
+49
-42
@@ -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,
|
||||||
await response(scope, receive, send)
|
algorithm="RS256",
|
||||||
return
|
audience=CLERK_PUBLISHABLE_KEY, # Use publishable key as audience
|
||||||
|
required_scopes=["yargi.read"] # Global scope requirement
|
||||||
# Add user info to scope for downstream processing
|
)
|
||||||
token = auth_header.split(" ")[1]
|
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
|
||||||
if token.startswith("eyJ"):
|
else:
|
||||||
import jwt
|
# Development: Generate RSA key pair for testing
|
||||||
try:
|
logger.warning("No Clerk credentials found - using development RSA key pair")
|
||||||
decoded = jwt.decode(token, options={"verify_signature": False})
|
dev_key_pair = RSAKeyPair.generate()
|
||||||
scope["user"] = {
|
bearer_auth = BearerAuthProvider(
|
||||||
"user_id": decoded.get("user_id") or decoded.get("sub"),
|
public_key=dev_key_pair.public_key,
|
||||||
"email": decoded.get("email"),
|
issuer="https://dev.yargimcp.com",
|
||||||
"scopes": decoded.get("scopes", ["read", "search"])
|
audience="dev-mcp-server",
|
||||||
}
|
required_scopes=["yargi.read"]
|
||||||
except:
|
)
|
||||||
pass
|
|
||||||
|
# Generate a test token for development
|
||||||
await self.app(scope, receive, send)
|
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 = [
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+36
-5
@@ -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,10 +238,14 @@ 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."""
|
||||||
app = FastMCP("Yargı MCP Server")
|
if auth:
|
||||||
logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)")
|
app = FastMCP("Yargı MCP Server", auth=auth)
|
||||||
|
logger.info("MCP server created with Bearer authentication enabled")
|
||||||
|
else:
|
||||||
|
app = FastMCP("Yargı MCP Server")
|
||||||
|
logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)")
|
||||||
return app
|
return app
|
||||||
|
|
||||||
# --- Module Imports ---
|
# --- Module Imports ---
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user