import json
from datetime import datetime
from pathlib import Path
from fastmcp import Context, FastMCP
from jsonschema import ValidationError, validate
from pydantic import Field
from typing_extensions import Annotated, Any, Dict, Optional
from auth.scope_registry import ScopeRegistry
from config.enhanced_logging import setup_logger
# Import validation utilities
from resources.service_list_resources import get_supported_services
# Import our custom types for consistent parameter definitions
from tools.common_types import ServiceTypeAnnotated
logger = setup_logger()
def setup_service_resources(main_mcp: FastMCP):
"""Register service resources with the main FastMCP server using FastMCP 2.10.6+ API."""
logger.info("🔧 Setting up Google service resources...")
# Register resources directly with main MCP instance using FastMCP 2.10.6+ approach
# This avoids the Context creation issues from using a separate local instance
logger.info("✅ Service resources setup completed (FastMCP 2.10.6+ compatible)")
# Load the JSON schema for Google Service Configuration
_SCHEMA_PATH = (
Path(__file__).parent.parent / "schemas" / "google_service_config_schema.json"
)
_SERVICE_CONFIG_SCHEMA: Optional[Dict[str, Any]] = None
def _load_schema():
"""Loads the JSON schema from file."""
global _SERVICE_CONFIG_SCHEMA
if _SERVICE_CONFIG_SCHEMA is None:
try:
with open(_SCHEMA_PATH, "r") as f:
_SERVICE_CONFIG_SCHEMA = json.load(f)
logger.info(f"Successfully loaded schema from {_SCHEMA_PATH}")
except FileNotFoundError:
logger.error(f"Schema file not found at {_SCHEMA_PATH}")
_SERVICE_CONFIG_SCHEMA = {} # Set to empty to avoid repeated attempts
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON schema from {_SCHEMA_PATH}: {e}")
_SERVICE_CONFIG_SCHEMA = {}
return _SERVICE_CONFIG_SCHEMA
def validate_service_config(config_data: Dict[str, Any]):
"""
Validates a service configuration dictionary against the predefined JSON schema.
Args:
config_data: The dictionary containing service configuration.
Raises:
ValidationError: If the config_data does not conform to the schema.
"""
schema = _load_schema()
if not schema:
logger.warning("Schema not loaded, skipping validation.")
return
try:
validate(instance=config_data, schema=schema)
logger.debug("Service configuration validated successfully.")
except ValidationError as e:
logger.error(
f"Service configuration validation failed: {e.message} at {e.path}"
)
raise
async def get_service_config(
ctx: Context,
service_type: ServiceTypeAnnotated,
api_version: Annotated[
Optional[str],
Field(
description="Optional specific API version (e.g., 'v1', 'v3')",
pattern=r"^v\d+$",
),
] = None,
) -> Dict[str, Any]:
"""
Retrieves the configuration for a specified Google service.
Args:
ctx: The FastMCP context.
service_type: The type of Google service (validated against supported services).
api_version: Optional. The specific API version to retrieve (e.g., 'v1', 'v3').
If not provided, the default or recommended version will be returned.
Returns:
A dictionary containing the service configuration details.
"""
# Normalize service type to lowercase
service_lower = service_type.lower()
# Validate service is supported
if service_lower not in get_supported_services():
raise ValueError(
f"Service '{service_type}' not supported. Available: {get_supported_services()}"
)
# Get endpoint information dynamically
endpoint = _get_service_endpoint(service_lower, api_version)
# Get scopes from the scope registry
try:
basic_scopes = ScopeRegistry.get_service_scopes(service_lower, "basic")
full_scopes = ScopeRegistry.get_service_scopes(service_lower, "full")
readonly_scopes = ScopeRegistry.get_service_scopes(service_lower, "readonly")
except ValueError:
# Fallback if service not in scope registry
basic_scopes = []
full_scopes = []
readonly_scopes = []
# Example structure for a service configuration
config_data = {
"service": service_lower,
"configuration": {
"api_name": service_lower,
"api_version": (
api_version if api_version else _get_default_api_version(service_lower)
),
"base_url": endpoint,
"discovery_url": f"{endpoint}$discovery/rest?version={api_version if api_version else _get_default_api_version(service_lower)}",
"scopes": {
"read": readonly_scopes,
"basic": basic_scopes,
"full": full_scopes,
},
"features": {
"batch_requests": True,
"watch_notifications": False,
"delegation": False,
},
"limits": {"requests_per_minute": 1000, "requests_per_day": 100000},
},
"metadata": {
"last_updated": datetime.now().isoformat(),
"documentation_url": f"https://developers.google.com/{service_lower}/api",
"authentication_required": True,
"tags": ["google", "service", "config", "metadata", "api", service_lower],
},
}
# Add service-specific details
if service_lower == "gmail":
config_data["configuration"]["features"]["watch_notifications"] = True
config_data["configuration"]["limits"]["attachment_size_mb"] = 25
config_data["metadata"]["tags"].extend(["email", "communication"])
elif service_lower == "drive":
config_data["configuration"]["features"]["delegation"] = True
config_data["metadata"]["tags"].extend(["storage", "files"])
elif service_lower == "photos":
config_data["configuration"]["features"]["batch_uploads"] = True
config_data["metadata"]["tags"].extend(["media", "images"])
# Additional service-specific configurations are now handled via scope registry
# Validate the generated configuration against the schema
try:
validate_service_config(config_data)
except ValidationError as e:
logger.error(
f"Generated service config for {service_lower} failed validation: {e.message}"
)
# Depending on severity, you might raise the error or return a partial/error response
raise ValueError(
f"Invalid service configuration generated for {service_lower}: {e.message}"
)
return config_data
# Helper functions for dynamic service configuration
def _get_service_endpoint(service: str, api_version: Optional[str] = None) -> str:
"""
Get the API endpoint for a service dynamically.
Args:
service: Service name (lowercase)
api_version: Optional API version
Returns:
Base API endpoint URL
"""
# Service-specific endpoint patterns
endpoint_patterns = {
"gmail": "https://gmail.googleapis.com/",
"drive": f"https://www.googleapis.com/drive/{api_version or 'v3'}/",
"calendar": f"https://www.googleapis.com/calendar/{api_version or 'v3'}/",
"docs": f"https://docs.googleapis.com/{api_version or 'v1'}/",
"sheets": f"https://sheets.googleapis.com/{api_version or 'v4'}/",
"forms": f"https://forms.googleapis.com/{api_version or 'v1'}/",
"slides": f"https://slides.googleapis.com/{api_version or 'v1'}/",
"chat": f"https://chat.googleapis.com/{api_version or 'v1'}/",
"photos": "https://photoslibrary.googleapis.com/",
"photoslibrary": "https://photoslibrary.googleapis.com/",
"admin": f"https://admin.googleapis.com/admin/directory/{api_version or 'v1'}/",
"tasks": f"https://tasks.googleapis.com/tasks/{api_version or 'v1'}/",
"youtube": f"https://www.googleapis.com/youtube/{api_version or 'v3'}/",
"script": f"https://script.googleapis.com/{api_version or 'v1'}/",
}
# Return specific pattern or generate default
if service in endpoint_patterns:
return endpoint_patterns[service]
else:
# Default pattern for unknown services
return f"https://www.googleapis.com/{service}/{api_version or 'v1'}/"
def _get_default_api_version(service: str) -> str:
"""
Get the default API version for a service.
Args:
service: Service name (lowercase)
Returns:
Default API version string
"""
default_versions = {
"gmail": "v1",
"drive": "v3",
"calendar": "v3",
"docs": "v1",
"sheets": "v4",
"forms": "v1",
"slides": "v1",
"chat": "v1",
"photos": "v1",
"photoslibrary": "v1",
"admin": "v1",
"tasks": "v1",
"youtube": "v3",
"script": "v1",
}
return default_versions.get(service, "v1")
# Additional resource templates for scopes and versions
async def get_service_scopes_by_group(
ctx: Context,
service_type: ServiceTypeAnnotated,
scope_group: Annotated[
str, Field(description="Scope group name (e.g., 'basic', 'full', 'readonly')")
],
) -> Dict[str, Any]:
"""
Retrieves OAuth scopes for a specified Google service and scope group.
Args:
ctx: The FastMCP context.
service_type: The type of Google service (validated).
scope_group: The scope group (e.g., 'basic', 'full', 'readonly').
Returns:
Dictionary with scope information.
"""
# Normalize service type
service_lower = service_type.lower()
# Validate service is supported
if service_lower not in get_supported_services():
raise ValueError(
f"Service '{service_type}' not supported. Available: {get_supported_services()}"
)
# Get scopes from the scope registry
try:
scopes = ScopeRegistry.get_service_scopes(service_lower, scope_group)
except ValueError as e:
logger.warning(f"Could not get scopes for {service_lower}/{scope_group}: {e}")
scopes = []
return {
"service_type": service_lower,
"scope_group": scope_group,
"scopes": scopes,
"description": f"Scopes for {service_lower} {scope_group} access.",
}
async def get_service_versions(
ctx: Context, service_type: ServiceTypeAnnotated
) -> Dict[str, Any]:
"""
Retrieves available API versions for a specified Google service.
Args:
ctx: The FastMCP context.
service_type: The type of Google service (validated).
Returns:
Dictionary with version information.
"""
# Normalize service type
service_lower = service_type.lower()
# Validate service is supported
if service_lower not in get_supported_services():
raise ValueError(
f"Service '{service_type}' not supported. Available: {get_supported_services()}"
)
# Known API versions for each service
versions = {
"gmail": ["v1"],
"drive": ["v2", "v3"],
"calendar": ["v3"],
"docs": ["v1"],
"sheets": ["v4"],
"forms": ["v1"],
"slides": ["v1"],
"chat": ["v1"],
"photos": ["v1"],
"photoslibrary": ["v1"],
"admin": ["v1"],
"tasks": ["v1"],
"youtube": ["v3"],
"script": ["v1"],
}
return {
"service_type": service_lower,
"available_versions": versions.get(service_lower, ["v1"]),
"default_version": _get_default_api_version(service_lower),
"description": f"Available API versions for {service_lower}.",
}
async def get_service_quota(
ctx: Context, service_type: ServiceTypeAnnotated
) -> Dict[str, Any]:
"""
Retrieves quota information for a specified Google service.
Args:
ctx: The FastMCP context.
service_type: The type of Google service (validated).
Returns:
Dictionary with quota information.
"""
# Normalize service type
service_lower = service_type.lower()
# Validate service is supported
if service_lower not in get_supported_services():
raise ValueError(
f"Service '{service_type}' not supported. Available: {get_supported_services()}"
)
# Quota information (these are typical values, actual quotas may vary by project)
quota_info = {
"gmail": {
"requests_per_minute": 1000,
"requests_per_day": 1000000,
"concurrent_requests": 100,
"quota_units": "per-user",
},
"drive": {
"requests_per_minute": 10000,
"requests_per_day": 10000000,
"concurrent_requests": 500,
"quota_units": "per-project",
},
"calendar": {
"requests_per_minute": 2000,
"requests_per_day": 1000000,
"concurrent_requests": 200,
"quota_units": "per-user",
},
"sheets": {
"requests_per_minute": 300,
"requests_per_100_seconds": 500,
"concurrent_requests": 100,
"quota_units": "per-project",
},
"docs": {
"requests_per_minute": 300,
"requests_per_100_seconds": 500,
"concurrent_requests": 100,
"quota_units": "per-project",
},
"photos": {
"requests_per_minute": 1000,
"requests_per_day": 50000,
"concurrent_requests": 50,
"quota_units": "per-user",
},
}
# Default quota for services not explicitly listed
default_quota = {
"requests_per_minute": 500,
"requests_per_day": 100000,
"concurrent_requests": 50,
"quota_units": "per-project",
}
return {
"service_type": service_lower,
"quota_limits": quota_info.get(service_lower, default_quota),
"description": f"Quota limits for {service_lower} API.",
"note": "Actual quotas may vary based on your Google Cloud project settings.",
}
async def get_service_endpoints(
ctx: Context, service_type: ServiceTypeAnnotated
) -> Dict[str, Any]:
"""
Retrieves API endpoints for a specified Google service.
This function dynamically generates endpoints based on the service type
and integrates with the scope registry for comprehensive service information.
Args:
ctx: The FastMCP context.
service_type: The type of Google service (validated).
Returns:
Dictionary with endpoint information.
"""
# Normalize service type
service_lower = service_type.lower()
# Validate service is supported
if service_lower not in get_supported_services():
raise ValueError(
f"Service '{service_type}' not supported. Available: {get_supported_services()}"
)
# Get the base endpoint
base_endpoint = _get_service_endpoint(service_lower)
default_version = _get_default_api_version(service_lower)
# Check if service exists in scope registry for additional info
has_scope_info = service_lower in ScopeRegistry.GOOGLE_API_SCOPES
return {
"service_type": service_lower,
"base_endpoint": base_endpoint,
"default_version": default_version,
"discovery_url": f"{base_endpoint}$discovery/rest?version={default_version}",
"oauth_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"has_scope_registry": has_scope_info,
"description": f"API endpoints for {service_lower} service.",
"documentation": f"https://developers.google.com/{service_lower}/api",
}