Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5fa0076f8 | ||
|
|
7a346ef3f6 | ||
|
|
217103f0b6 | ||
|
|
1fbcb65031 | ||
|
|
9e40671798 | ||
|
|
6c8a614872 | ||
|
|
861d9e86ef | ||
|
|
c4b5d3608a | ||
|
|
38e0cc032b | ||
|
|
2c1b8c6f9d | ||
|
|
92f04fbab6 | ||
|
|
515347e29c | ||
|
|
ebefe22a4c | ||
|
|
c93244ee10 | ||
|
|
d84f8a2c88 |
+77
-189
@@ -19,9 +19,10 @@ 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.responses import Response
|
||||||
|
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
|
||||||
@@ -36,23 +37,69 @@ BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
|
|||||||
# Setup logging
|
# Setup logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Configure CORS middleware
|
# Configure CORS and Auth middleware
|
||||||
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
|
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 = [
|
custom_middleware = [
|
||||||
Middleware(
|
Middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=cors_origins,
|
allow_origins=cors_origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST", "OPTIONS"],
|
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"],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create MCP Starlette sub-application (without auth wrapper)
|
# Create MCP app with Bearer authentication
|
||||||
mcp_app = mcp_server.http_app(
|
mcp_server = create_app(auth=bearer_auth)
|
||||||
path="/",
|
|
||||||
middleware=custom_middleware
|
# 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
|
# Configure JSON encoder for proper Turkish character support
|
||||||
import json
|
import json
|
||||||
@@ -74,13 +121,12 @@ class UTF8JSONResponse(JSONResponse):
|
|||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
|
|
||||||
# Create FastAPI wrapper application with MCP lifespan
|
# Create FastAPI wrapper application
|
||||||
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,
|
||||||
lifespan=mcp_app.lifespan, # MCP app lifespan
|
|
||||||
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
|
default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,141 +153,7 @@ async def custom_401_handler(request: Request, exc: HTTPException):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Mount MCP app as sub-application at /mcp-server to avoid path conflicts
|
# FastAPI health check endpoint - BEFORE mounting MCP app
|
||||||
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
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint for monitoring"""
|
"""Health check endpoint for monitoring"""
|
||||||
@@ -253,6 +165,22 @@ async def health_check():
|
|||||||
"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
|
||||||
|
@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
|
# FastAPI root endpoint
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
@@ -374,48 +302,8 @@ async def oauth_authorization_server_root():
|
|||||||
"resource_documentation": f"{BASE_URL}/mcp"
|
"resource_documentation": f"{BASE_URL}/mcp"
|
||||||
})
|
})
|
||||||
|
|
||||||
# MCP endpoint info for GET requests (ChatGPT compatibility)
|
# Note: GET /mcp is handled by the mounted MCP app itself
|
||||||
@app.get("/mcp")
|
# This prevents 405 Method Not Allowed errors on POST requests
|
||||||
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>"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 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")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class BedestenSearchData(BaseModel):
|
|||||||
pageSize: int = Field(..., description="Results per page (1-10)")
|
pageSize: int = Field(..., description="Results per page (1-10)")
|
||||||
pageNumber: int = Field(..., description="Page number (1-indexed)")
|
pageNumber: int = Field(..., description="Page number (1-indexed)")
|
||||||
itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)")
|
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="""
|
birimAdi: BirimAdiEnum = Field("ALL", description="""
|
||||||
Chamber filter (optional). Abbreviated values with Turkish names:
|
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)
|
• 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)
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ LOG_LEVEL = "info"
|
|||||||
[http_service]
|
[http_service]
|
||||||
internal_port = 8000
|
internal_port = 8000
|
||||||
force_https = true
|
force_https = true
|
||||||
auto_stop_machines = 'stop'
|
auto_stop_machines = 'off'
|
||||||
auto_start_machines = true
|
auto_start_machines = true
|
||||||
min_machines_running = 0
|
min_machines_running = 1
|
||||||
processes = ['app']
|
processes = ['app']
|
||||||
|
|
||||||
[[vm]]
|
[[vm]]
|
||||||
|
|||||||
+55
-16
@@ -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,9 +238,25 @@ 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 basic FastMCP app without authentication wrapper"""
|
"""Create FastMCP app with standard capabilities and optional auth."""
|
||||||
return FastMCP("Yargı MCP Server")
|
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 ---
|
# --- Module Imports ---
|
||||||
from yargitay_mcp_module.client import YargitayOfficialApiClient
|
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 ---
|
# Placeholder app for decorators - will be replaced in create_app() after all tools are defined
|
||||||
token_counter = TokenCountingMiddleware()
|
app = FastMCP("Yargı MCP Server Placeholder")
|
||||||
app.add_middleware(token_counter)
|
|
||||||
logger.info("Token counting middleware added to MCP server")
|
|
||||||
|
|
||||||
# --- Tool Documentation Resources ---
|
# --- Tool Documentation Resources ---
|
||||||
@app.resource("docs://tools/yargitay")
|
@app.resource("docs://tools/yargitay")
|
||||||
@@ -1773,17 +1795,16 @@ async def get_rekabet_kurumu_document(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def search_bedesten_unified(
|
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)
|
• Simple: "mülkiyet hakkı" (finds both words)
|
||||||
• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)
|
• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)
|
||||||
• Required term: "+mülkiyet hakkı" (must contain mülkiyet)
|
• Required term: "+mülkiyet hakkı" (must contain mülkiyet)
|
||||||
• Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira)
|
• Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira)
|
||||||
• Wildcard: "mülk*" (mülkiyet, mülk, etc.)
|
• Boolean AND: "mülkiyet AND hak" (both terms required)
|
||||||
• Fuzzy: "mülkiyet~" (similar spelling variations)
|
• Boolean OR: "mülkiyet OR tapu" (either term acceptable)
|
||||||
• Multiple terms: "mülkiyet AND hak" (both terms required)
|
• Boolean NOT: "mülkiyet NOT satış" (contains mülkiyet but not satış)
|
||||||
• Either term: "mülkiyet OR tapu" (either term acceptable)
|
NOTE: Wildcards (*,?), regex patterns (/regex/), fuzzy search (~), and proximity search are NOT supported.
|
||||||
• Proximity: "\"mülkiyet hakkı\"~5" (words within 5 positions)
|
|
||||||
• Regex: "/mülk.*/" (pattern matching)
|
|
||||||
For best results, use exact phrases with quotes for legal terms."""),
|
For best results, use exact phrases with quotes for legal terms."""),
|
||||||
court_types: List[BedestenCourtTypeEnum] = Field(
|
court_types: List[BedestenCourtTypeEnum] = Field(
|
||||||
default=["YARGITAYKARARI", "DANISTAYKARAR"],
|
default=["YARGITAYKARARI", "DANISTAYKARAR"],
|
||||||
@@ -1801,6 +1822,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 +1854,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)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "yargi-mcp"
|
name = "yargi-mcp"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
description = "MCP Server For Turkish Legal Databases"
|
description = "MCP Server For Turkish Legal Databases"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
Reference in New Issue
Block a user