Spaces:
Sleeping
Sleeping
docs: add comprehensive docstrings to all Python files
Browse files- hf_space/app.py +86 -4
- hf_space/mcp_client.py +109 -26
- mcp_server/mcp_server.py +17 -8
- mcp_server/tools/distance_tool.py +81 -5
- mcp_server/tools/llm_agent_tool.py +48 -4
- mcp_server/tools/location_tool.py +37 -4
- mcp_server/tools/marine_data_tool.py +69 -4
- mcp_server/tools/spot_finder_tool.py +144 -11
- mcp_server/tools/stormglass_tool.py +49 -3
- mcp_server/tools/surf_eval_tool.py +86 -3
hf_space/app.py
CHANGED
|
@@ -1,11 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from mcp_client import find_best_spots
|
| 3 |
import os
|
| 4 |
import json
|
| 5 |
import time
|
| 6 |
|
| 7 |
-
def format_spot_results(results, ai_summary="", ai_reasoning=""):
|
| 8 |
-
"""Format surf spot results for display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
if not results:
|
| 10 |
return "No surf spots found in your area.", "", ""
|
| 11 |
|
|
@@ -108,8 +167,31 @@ def format_spot_results(results, ai_summary="", ai_reasoning=""):
|
|
| 108 |
|
| 109 |
return spots_html, ai_summary_html, ai_reasoning_html
|
| 110 |
|
| 111 |
-
def run_surf_finder(location, max_distance, num_spots, skill_level, board_type):
|
| 112 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
try:
|
| 114 |
import os
|
| 115 |
import time
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Surf Spot Finder Gradio Interface - Interactive Web UI.
|
| 3 |
+
|
| 4 |
+
This module provides a user-friendly Gradio web interface for the Surf Spot
|
| 5 |
+
Finder application. It connects to the MCP server via the mcp_client and
|
| 6 |
+
presents surf spot recommendations with enhanced UX and AI reasoning.
|
| 7 |
+
|
| 8 |
+
Features:
|
| 9 |
+
- Interactive location search with autocomplete
|
| 10 |
+
- Customizable search parameters (radius, skill level, board type)
|
| 11 |
+
- Real-time surf spot recommendations
|
| 12 |
+
- AI-powered analysis and explanations
|
| 13 |
+
- Responsive design with enhanced CSS styling
|
| 14 |
+
- Loading animations and error handling
|
| 15 |
+
- Example locations for quick testing
|
| 16 |
+
|
| 17 |
+
The interface demonstrates MCP in Action by:
|
| 18 |
+
- Orchestrating multiple MCP tools seamlessly
|
| 19 |
+
- Presenting AI reasoning in natural language
|
| 20 |
+
- Providing real-world surf recommendations
|
| 21 |
+
- Showcasing autonomous agent behavior
|
| 22 |
+
|
| 23 |
+
UI Components:
|
| 24 |
+
- Location input with validation
|
| 25 |
+
- Search radius slider (10-200km)
|
| 26 |
+
- Number of results selector (1-10)
|
| 27 |
+
- Skill level dropdown (beginner/intermediate/advanced)
|
| 28 |
+
- Board type selection (optional)
|
| 29 |
+
- Results display with scoring breakdown
|
| 30 |
+
- AI reasoning accordion for detailed analysis
|
| 31 |
+
|
| 32 |
+
Example Usage:
|
| 33 |
+
The interface is deployed on Hugging Face Spaces and can be run locally:
|
| 34 |
+
>>> python app.py # Starts Gradio server on localhost:7860
|
| 35 |
+
|
| 36 |
+
Author: Surf Spot Finder Team
|
| 37 |
+
License: MIT
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
import gradio as gr
|
| 41 |
from mcp_client import find_best_spots
|
| 42 |
import os
|
| 43 |
import json
|
| 44 |
import time
|
| 45 |
|
| 46 |
+
def format_spot_results(results, ai_summary: str = "", ai_reasoning: str = "") -> tuple[str, str, str]:
|
| 47 |
+
"""Format surf spot results for rich HTML display.
|
| 48 |
+
|
| 49 |
+
Converts raw surf spot data into formatted HTML with enhanced styling,
|
| 50 |
+
score visualization, and condition summaries. Includes AI reasoning
|
| 51 |
+
formatting with markdown conversion and section highlighting.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
results: List of surf spot dicts with scores and conditions.
|
| 55 |
+
ai_summary: Brief AI-generated summary (currently unused).
|
| 56 |
+
ai_reasoning: Detailed AI analysis with markdown formatting.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Tuple containing (formatted_spots_html, ai_summary_html, ai_reasoning_html)
|
| 60 |
+
|
| 61 |
+
Features:
|
| 62 |
+
- Color-coded scoring (green/yellow/red based on score)
|
| 63 |
+
- Responsive card layout with spot details
|
| 64 |
+
- Markdown to HTML conversion for AI reasoning
|
| 65 |
+
- Section headers with emoji and enhanced styling
|
| 66 |
+
- Scrollable content for mobile compatibility
|
| 67 |
+
"""
|
| 68 |
if not results:
|
| 69 |
return "No surf spots found in your area.", "", ""
|
| 70 |
|
|
|
|
| 167 |
|
| 168 |
return spots_html, ai_summary_html, ai_reasoning_html
|
| 169 |
|
| 170 |
+
def run_surf_finder(location: str, max_distance: int, num_spots: int, skill_level: str, board_type: str) -> tuple[str, str]:
|
| 171 |
+
"""Execute surf spot search and return formatted results.
|
| 172 |
+
|
| 173 |
+
Main function that coordinates the surf spot search process,
|
| 174 |
+
handles user inputs, calls the MCP client, and formats results
|
| 175 |
+
for display in the Gradio interface.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
location: User's location string (address, coordinates, place name).
|
| 179 |
+
max_distance: Search radius in kilometers.
|
| 180 |
+
num_spots: Number of surf spots to return.
|
| 181 |
+
skill_level: User's surfing skill level (beginner/intermediate/advanced).
|
| 182 |
+
board_type: Board preference (shortboard/longboard/funboard/etc).
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
Tuple of (formatted_results_html, ai_reasoning_html) for display.
|
| 186 |
+
|
| 187 |
+
Process:
|
| 188 |
+
1. Validates and cleans user inputs
|
| 189 |
+
2. Builds preferences dictionary
|
| 190 |
+
3. Calls MCP client with parameters
|
| 191 |
+
4. Handles errors gracefully with user-friendly messages
|
| 192 |
+
5. Formats successful results for rich display
|
| 193 |
+
6. Returns HTML content for Gradio components
|
| 194 |
+
"""
|
| 195 |
try:
|
| 196 |
import os
|
| 197 |
import time
|
hf_space/mcp_client.py
CHANGED
|
@@ -1,4 +1,36 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import asyncio
|
| 3 |
import sys
|
| 4 |
from typing import Dict, Any, Optional
|
|
@@ -12,24 +44,51 @@ try:
|
|
| 12 |
except ImportError:
|
| 13 |
DIRECT_MODE = False
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
# # Default to Modal deployment URL, fallback to local FastAPI server
|
| 17 |
-
# MODAL_URL = os.environ.get("MODAL_URL")
|
| 18 |
-
# BASE = MODAL_URL or os.environ.get("MCP_SERVER_URL", "http://localhost:8080")
|
| 19 |
-
|
| 20 |
-
# Set to base (local implementation) for now
|
| 21 |
BASE = os.environ.get("MCP_SERVER_URL", "http://localhost:8080")
|
| 22 |
-
MODAL_URL = os.environ.get("MODAL_URL", None)
|
| 23 |
|
| 24 |
def post(path: str, payload: Dict[str, Any], timeout: int = 120) -> Dict[str, Any]:
|
| 25 |
-
"""Make HTTP request to MCP server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
url = f"{BASE}{path}"
|
| 27 |
resp = requests.post(url, json=payload, timeout=timeout)
|
| 28 |
resp.raise_for_status()
|
| 29 |
return resp.json()
|
| 30 |
|
| 31 |
async def find_best_spots_direct(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 32 |
-
"""Direct call to MCP server tools
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
finder = SurfSpotFinder()
|
| 34 |
input_data = SpotFinderInput(
|
| 35 |
user_location=user_location,
|
|
@@ -62,20 +121,29 @@ async def find_best_spots_direct(user_location: str, max_distance_km: int = 50,
|
|
| 62 |
}
|
| 63 |
|
| 64 |
def find_best_spots_http(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 65 |
-
"""HTTP call to deployed MCP server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
payload = {
|
| 67 |
-
"location": user_location,
|
| 68 |
"max_distance": max_distance_km,
|
| 69 |
"num_spots": top_n,
|
| 70 |
"preferences": prefs or {}
|
| 71 |
}
|
| 72 |
try:
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
-
response = post("", payload) # Modal URL already points to the specific function
|
| 76 |
-
else:
|
| 77 |
-
# Fallback to local FastAPI endpoint
|
| 78 |
-
response = post("/tool/find_surf_spots", payload)
|
| 79 |
|
| 80 |
if response.get("ok"):
|
| 81 |
return {
|
|
@@ -99,15 +167,30 @@ def find_best_spots_http(user_location: str, max_distance_km: int = 50, top_n: i
|
|
| 99 |
}
|
| 100 |
|
| 101 |
def find_best_spots(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 102 |
-
"""Main interface
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
# Priority
|
| 110 |
-
|
| 111 |
print("🏠 Using direct local development mode")
|
| 112 |
# Run async function in sync context
|
| 113 |
loop = None
|
|
@@ -119,7 +202,7 @@ def find_best_spots(user_location: str, max_distance_km: int = 50, top_n: int =
|
|
| 119 |
|
| 120 |
return loop.run_until_complete(find_best_spots_direct(user_location, max_distance_km, top_n, prefs))
|
| 121 |
|
| 122 |
-
# Priority
|
| 123 |
else:
|
| 124 |
print("🌐 Using HTTP mode fallback")
|
| 125 |
return find_best_spots_http(user_location, max_distance_km, top_n, prefs)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Client for Surf Spot Finder - Direct Mode with HTTP Fallback.
|
| 3 |
+
|
| 4 |
+
This module provides a smart client interface that connects to the MCP server
|
| 5 |
+
using the most appropriate method available. It prioritizes direct Python
|
| 6 |
+
imports for local development and falls back to HTTP for deployed scenarios.
|
| 7 |
+
|
| 8 |
+
Execution Modes (Priority Order):
|
| 9 |
+
1. Direct Mode: Local development with direct Python imports (primary)
|
| 10 |
+
2. HTTP Mode: Communication with deployed FastAPI server (fallback)
|
| 11 |
+
|
| 12 |
+
The client automatically detects the available mode and provides a unified
|
| 13 |
+
interface for the Gradio UI, ensuring seamless operation across different
|
| 14 |
+
deployment scenarios.
|
| 15 |
+
|
| 16 |
+
Environment Variables:
|
| 17 |
+
MCP_SERVER_URL: HTTP server URL (default: http://localhost:8080)
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
>>> result = find_best_spots(
|
| 21 |
+
... user_location="Málaga, Spain",
|
| 22 |
+
... max_distance_km=50,
|
| 23 |
+
... top_n=3,
|
| 24 |
+
... prefs={"skill_level": "intermediate"}
|
| 25 |
+
... )
|
| 26 |
+
>>> print(f"Found {len(result['results'])} surf spots")
|
| 27 |
+
|
| 28 |
+
Author: Surf Spot Finder Team
|
| 29 |
+
License: MIT
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import os
|
| 33 |
+
import requests
|
| 34 |
import asyncio
|
| 35 |
import sys
|
| 36 |
from typing import Dict, Any, Optional
|
|
|
|
| 44 |
except ImportError:
|
| 45 |
DIRECT_MODE = False
|
| 46 |
|
| 47 |
+
# MCP Server URL for HTTP mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
BASE = os.environ.get("MCP_SERVER_URL", "http://localhost:8080")
|
|
|
|
| 49 |
|
| 50 |
def post(path: str, payload: Dict[str, Any], timeout: int = 120) -> Dict[str, Any]:
|
| 51 |
+
"""Make HTTP request to MCP server endpoint.
|
| 52 |
+
|
| 53 |
+
Sends POST request with JSON payload to the configured server endpoint.
|
| 54 |
+
Used for HTTP mode communication with deployed MCP servers.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
path: API endpoint path (e.g., "/tools/find_surf_spots").
|
| 58 |
+
payload: Request data as dictionary.
|
| 59 |
+
timeout: Request timeout in seconds (default: 120).
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Response JSON data as dictionary.
|
| 63 |
+
|
| 64 |
+
Raises:
|
| 65 |
+
requests.exceptions.RequestException: On HTTP errors or timeouts.
|
| 66 |
+
"""
|
| 67 |
url = f"{BASE}{path}"
|
| 68 |
resp = requests.post(url, json=payload, timeout=timeout)
|
| 69 |
resp.raise_for_status()
|
| 70 |
return resp.json()
|
| 71 |
|
| 72 |
async def find_best_spots_direct(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 73 |
+
"""Direct call to MCP server tools for local development.
|
| 74 |
+
|
| 75 |
+
Uses direct Python imports to call MCP tools without HTTP overhead.
|
| 76 |
+
This mode provides the fastest execution and best debugging experience
|
| 77 |
+
for local development and testing.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
user_location: User's location string (address, coordinates, etc.).
|
| 81 |
+
max_distance_km: Maximum search radius in kilometers.
|
| 82 |
+
top_n: Number of top surf spots to return.
|
| 83 |
+
prefs: User preferences including skill level, board type.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
Dict containing surf spot results in standardized format.
|
| 87 |
+
|
| 88 |
+
Note:
|
| 89 |
+
This function is async and must be called with asyncio.run()
|
| 90 |
+
or within an existing async context.
|
| 91 |
+
"""
|
| 92 |
finder = SurfSpotFinder()
|
| 93 |
input_data = SpotFinderInput(
|
| 94 |
user_location=user_location,
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
def find_best_spots_http(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 124 |
+
"""HTTP call to deployed MCP server via FastAPI.
|
| 125 |
+
|
| 126 |
+
Makes HTTP request to a deployed FastAPI server running the MCP tools.
|
| 127 |
+
Used when direct imports are not available or for deployed scenarios.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
user_location: User's location string.
|
| 131 |
+
max_distance_km: Maximum search radius in kilometers.
|
| 132 |
+
top_n: Number of top surf spots to return.
|
| 133 |
+
prefs: User preferences dict.
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
Dict with standardized surf spot results or error information.
|
| 137 |
+
"""
|
| 138 |
payload = {
|
| 139 |
+
"location": user_location,
|
| 140 |
"max_distance": max_distance_km,
|
| 141 |
"num_spots": top_n,
|
| 142 |
"preferences": prefs or {}
|
| 143 |
}
|
| 144 |
try:
|
| 145 |
+
# Call FastAPI endpoint
|
| 146 |
+
response = post("/tool/find_surf_spots", payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
if response.get("ok"):
|
| 149 |
return {
|
|
|
|
| 167 |
}
|
| 168 |
|
| 169 |
def find_best_spots(user_location: str, max_distance_km: int = 50, top_n: int = 3, prefs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 170 |
+
"""Main interface for surf spot recommendations with intelligent mode detection.
|
| 171 |
|
| 172 |
+
Automatically selects the best available execution mode:
|
| 173 |
+
1. Direct Mode: Local development with direct Python imports
|
| 174 |
+
2. HTTP Mode: Deployed FastAPI server communication
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
user_location: User's location (address, coordinates, place name).
|
| 178 |
+
max_distance_km: Search radius in kilometers (default: 50).
|
| 179 |
+
top_n: Number of results to return (default: 3).
|
| 180 |
+
prefs: User preferences dict with skill_level, board_type, etc.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
Dict containing:
|
| 184 |
+
- ok: Success boolean
|
| 185 |
+
- results: List of ranked surf spots
|
| 186 |
+
- user_location: Resolved coordinates
|
| 187 |
+
- ai_summary: Brief AI analysis
|
| 188 |
+
- ai_reasoning: Detailed AI explanations
|
| 189 |
+
- error: Error message if ok=False
|
| 190 |
+
"""
|
| 191 |
|
| 192 |
+
# Priority 1: Direct local mode (development)
|
| 193 |
+
if DIRECT_MODE:
|
| 194 |
print("🏠 Using direct local development mode")
|
| 195 |
# Run async function in sync context
|
| 196 |
loop = None
|
|
|
|
| 202 |
|
| 203 |
return loop.run_until_complete(find_best_spots_direct(user_location, max_distance_km, top_n, prefs))
|
| 204 |
|
| 205 |
+
# Priority 2: HTTP fallback
|
| 206 |
else:
|
| 207 |
print("🌐 Using HTTP mode fallback")
|
| 208 |
return find_best_spots_http(user_location, max_distance_km, top_n, prefs)
|
mcp_server/mcp_server.py
CHANGED
|
@@ -1,8 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
-
MCP Server Configuration for Surf Spot Finder
|
| 3 |
|
| 4 |
-
This module
|
| 5 |
-
surf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import json
|
|
@@ -67,8 +79,5 @@ async def read_resource(uri: str) -> str:
|
|
| 67 |
raise ValueError(f"Unknown resource URI: {uri}")
|
| 68 |
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
|
| 72 |
-
# @app.get("/tools")
|
| 73 |
-
# def list_tools():
|
| 74 |
-
# return TOOLS
|
|
|
|
| 1 |
"""
|
| 2 |
+
MCP Server Configuration for Surf Spot Finder.
|
| 3 |
|
| 4 |
+
This module implements the Model Context Protocol server that exposes
|
| 5 |
+
surf spot data as MCP resources. It provides the core resource handlers
|
| 6 |
+
for listing and reading surf spot information.
|
| 7 |
+
|
| 8 |
+
The server exposes:
|
| 9 |
+
- surf://spots/database: 22 world-class surf spots with metadata
|
| 10 |
+
|
| 11 |
+
Example:
|
| 12 |
+
>>> resources = await list_resources()
|
| 13 |
+
>>> content = await read_resource("surf://spots/database")
|
| 14 |
+
>>> spots = json.loads(content)["spots"]
|
| 15 |
+
|
| 16 |
+
Author: Surf Spot Finder Team
|
| 17 |
+
License: MIT
|
| 18 |
"""
|
| 19 |
|
| 20 |
import json
|
|
|
|
| 79 |
raise ValueError(f"Unknown resource URI: {uri}")
|
| 80 |
|
| 81 |
|
| 82 |
+
# Export server instance for use by other modules
|
| 83 |
+
__all__ = ['server', 'list_resources', 'read_resource']
|
|
|
|
|
|
|
|
|
mcp_server/tools/distance_tool.py
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import Dict, Any
|
| 3 |
import math
|
|
@@ -5,26 +35,72 @@ import math
|
|
| 5 |
|
| 6 |
class DistanceTool:
|
| 7 |
"""
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
class DistanceInput(BaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
lat1: float
|
| 15 |
lon1: float
|
| 16 |
lat2: float
|
| 17 |
lon2: float
|
| 18 |
|
| 19 |
async def run(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
inp = self.DistanceInput(**input_data)
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
R = 6371
|
| 24 |
|
|
|
|
| 25 |
d_lat = math.radians(inp.lat2 - inp.lat1)
|
| 26 |
d_lon = math.radians(inp.lon2 - inp.lon1)
|
| 27 |
|
|
|
|
| 28 |
a = (
|
| 29 |
math.sin(d_lat / 2) ** 2
|
| 30 |
+ math.cos(math.radians(inp.lat1))
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Distance Calculation Tool - Haversine Formula Implementation.
|
| 3 |
+
|
| 4 |
+
This module provides accurate distance calculations between geographic
|
| 5 |
+
coordinates using the haversine formula for spherical distance calculation.
|
| 6 |
+
It's used for filtering surf spots by proximity to user location.
|
| 7 |
+
|
| 8 |
+
The haversine formula accounts for the Earth's spherical shape and
|
| 9 |
+
provides distance calculations accurate to within a few meters for
|
| 10 |
+
most surf spot finding applications.
|
| 11 |
+
|
| 12 |
+
Formula: d = 2r * arcsin(sqrt(sin²(Δφ/2) + cos(φ1) * cos(φ2) * sin²(Δλ/2)))
|
| 13 |
+
Where:
|
| 14 |
+
- φ is latitude in radians
|
| 15 |
+
- λ is longitude in radians
|
| 16 |
+
- r is Earth's radius (6371 km)
|
| 17 |
+
- Δφ, Δλ are differences in latitude and longitude
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
>>> tool = DistanceTool()
|
| 21 |
+
>>> result = await tool.run({
|
| 22 |
+
... "lat1": 36.72, "lon1": -4.42, # Málaga
|
| 23 |
+
... "lat2": 36.18, "lon2": -5.92 # Tarifa
|
| 24 |
+
... })
|
| 25 |
+
>>> print(f"Distance: {result['distance_km']:.1f}km")
|
| 26 |
+
|
| 27 |
+
Author: Surf Spot Finder Team
|
| 28 |
+
License: MIT
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
from pydantic import BaseModel
|
| 32 |
from typing import Dict, Any
|
| 33 |
import math
|
|
|
|
| 35 |
|
| 36 |
class DistanceTool:
|
| 37 |
"""
|
| 38 |
+
Geographic distance calculation tool using haversine formula.
|
| 39 |
+
|
| 40 |
+
Computes spherical distances between coordinate pairs on Earth's surface.
|
| 41 |
+
Provides accurate results for surf spot proximity filtering and ranking.
|
| 42 |
+
|
| 43 |
+
The haversine formula is well-suited for this use case because:
|
| 44 |
+
- Accurate for distances up to several hundred kilometers
|
| 45 |
+
- Computationally efficient
|
| 46 |
+
- Accounts for Earth's spherical shape
|
| 47 |
+
- Suitable for surf spot search radii (typically 10-100km)
|
| 48 |
+
|
| 49 |
+
Attributes:
|
| 50 |
+
Earth radius is set to 6371 km (mean radius)
|
| 51 |
+
|
| 52 |
+
Example:
|
| 53 |
+
>>> tool = DistanceTool()
|
| 54 |
+
>>> input_data = {
|
| 55 |
+
... "lat1": 40.7128, "lon1": -74.0060, # NYC
|
| 56 |
+
... "lat2": 40.7589, "lon2": -73.9851 # Central Park
|
| 57 |
+
... }
|
| 58 |
+
>>> result = await tool.run(input_data)
|
| 59 |
"""
|
| 60 |
|
| 61 |
class DistanceInput(BaseModel):
|
| 62 |
+
"""Input schema for distance calculation.
|
| 63 |
+
|
| 64 |
+
Attributes:
|
| 65 |
+
lat1: Origin latitude in decimal degrees (-90 to 90).
|
| 66 |
+
lon1: Origin longitude in decimal degrees (-180 to 180).
|
| 67 |
+
lat2: Destination latitude in decimal degrees (-90 to 90).
|
| 68 |
+
lon2: Destination longitude in decimal degrees (-180 to 180).
|
| 69 |
+
"""
|
| 70 |
lat1: float
|
| 71 |
lon1: float
|
| 72 |
lat2: float
|
| 73 |
lon2: float
|
| 74 |
|
| 75 |
async def run(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 76 |
+
"""Calculate spherical distance between two coordinate pairs.
|
| 77 |
+
|
| 78 |
+
Implements the haversine formula for accurate distance calculation
|
| 79 |
+
on Earth's surface. Suitable for surf spot proximity filtering.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
input_data: Dict containing lat1, lon1, lat2, lon2 coordinates.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Dict with 'distance_km' key containing distance in kilometers.
|
| 86 |
+
|
| 87 |
+
Example:
|
| 88 |
+
>>> result = await tool.run({
|
| 89 |
+
... "lat1": 36.72, "lon1": -4.42,
|
| 90 |
+
... "lat2": 36.18, "lon2": -5.92
|
| 91 |
+
... })
|
| 92 |
+
>>> print(result["distance_km"]) # 142.3
|
| 93 |
+
"""
|
| 94 |
inp = self.DistanceInput(**input_data)
|
| 95 |
|
| 96 |
+
# Earth's mean radius in kilometers
|
| 97 |
+
R = 6371
|
| 98 |
|
| 99 |
+
# Convert latitude and longitude differences to radians
|
| 100 |
d_lat = math.radians(inp.lat2 - inp.lat1)
|
| 101 |
d_lon = math.radians(inp.lon2 - inp.lon1)
|
| 102 |
|
| 103 |
+
# Haversine formula calculation
|
| 104 |
a = (
|
| 105 |
math.sin(d_lat / 2) ** 2
|
| 106 |
+ math.cos(math.radians(inp.lat1))
|
mcp_server/tools/llm_agent_tool.py
CHANGED
|
@@ -1,6 +1,35 @@
|
|
| 1 |
"""
|
| 2 |
-
LLM Agent Tool for Surf Spot
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
@@ -21,7 +50,14 @@ logger = logging.getLogger(__name__)
|
|
| 21 |
|
| 22 |
|
| 23 |
class LLMAgentInput(BaseModel):
|
| 24 |
-
"""Input schema for LLM agent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
user_location: str = Field(description="User's location")
|
| 26 |
user_preferences: Dict[str, Any] = Field(description="User surfing preferences")
|
| 27 |
surf_spots: List[Dict[str, Any]] = Field(description="Evaluated surf spots with scores")
|
|
@@ -29,7 +65,15 @@ class LLMAgentInput(BaseModel):
|
|
| 29 |
|
| 30 |
|
| 31 |
class LLMAgentOutput(BaseModel):
|
| 32 |
-
"""Output schema for LLM agent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
success: bool
|
| 34 |
summary: str = ""
|
| 35 |
reasoning: str = ""
|
|
|
|
| 1 |
"""
|
| 2 |
+
LLM Agent Tool for Intelligent Surf Spot Analysis.
|
| 3 |
+
|
| 4 |
+
This module provides AI-powered reasoning and natural language generation
|
| 5 |
+
for surf spot recommendations. It demonstrates autonomous agent behavior
|
| 6 |
+
by analyzing surf conditions and generating human-like explanations.
|
| 7 |
+
|
| 8 |
+
The agent supports multiple LLM providers with intelligent fallbacks:
|
| 9 |
+
1. OpenAI GPT-4 (primary)
|
| 10 |
+
2. Anthropic Claude (secondary)
|
| 11 |
+
3. OpenRouter (multi-model access)
|
| 12 |
+
4. Rule-based reasoning (always available)
|
| 13 |
+
|
| 14 |
+
Key capabilities:
|
| 15 |
+
- Autonomous analysis of surf conditions
|
| 16 |
+
- Natural language explanation generation
|
| 17 |
+
- Safety-focused recommendations based on skill level
|
| 18 |
+
- Multi-factor reasoning about wave, wind, and swell
|
| 19 |
+
- Contextual advice for different surf scenarios
|
| 20 |
+
|
| 21 |
+
Example:
|
| 22 |
+
>>> agent = SurfLLMAgent()
|
| 23 |
+
>>> input_data = LLMAgentInput(
|
| 24 |
+
... user_location="Tarifa, Spain",
|
| 25 |
+
... user_preferences={"skill_level": "beginner"},
|
| 26 |
+
... surf_spots=evaluated_spots
|
| 27 |
+
... )
|
| 28 |
+
>>> result = await agent.run(input_data)
|
| 29 |
+
>>> print(result.reasoning) # AI-generated explanation
|
| 30 |
+
|
| 31 |
+
Author: Surf Spot Finder Team
|
| 32 |
+
License: MIT
|
| 33 |
"""
|
| 34 |
|
| 35 |
import os
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
class LLMAgentInput(BaseModel):
|
| 53 |
+
"""Input schema for the LLM agent tool.
|
| 54 |
+
|
| 55 |
+
Attributes:
|
| 56 |
+
user_location: User's location for contextual recommendations.
|
| 57 |
+
user_preferences: Dict with skill_level, board_type, etc.
|
| 58 |
+
surf_spots: List of evaluated spots with scores and conditions.
|
| 59 |
+
reasoning_task: Type of analysis (default: "recommendation").
|
| 60 |
+
"""
|
| 61 |
user_location: str = Field(description="User's location")
|
| 62 |
user_preferences: Dict[str, Any] = Field(description="User surfing preferences")
|
| 63 |
surf_spots: List[Dict[str, Any]] = Field(description="Evaluated surf spots with scores")
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
class LLMAgentOutput(BaseModel):
|
| 68 |
+
"""Output schema for LLM agent results.
|
| 69 |
+
|
| 70 |
+
Attributes:
|
| 71 |
+
success: Whether AI analysis completed successfully.
|
| 72 |
+
summary: Brief recommendation summary (1-2 sentences).
|
| 73 |
+
reasoning: Detailed AI analysis with explanations.
|
| 74 |
+
recommendations: List of specific actionable advice.
|
| 75 |
+
error: Error message if analysis failed.
|
| 76 |
+
"""
|
| 77 |
success: bool
|
| 78 |
summary: str = ""
|
| 79 |
reasoning: str = ""
|
mcp_server/tools/location_tool.py
CHANGED
|
@@ -1,6 +1,28 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from typing import Dict, Optional
|
|
@@ -25,14 +47,25 @@ logger = logging.getLogger(__name__)
|
|
| 25 |
# ---------------------- Input / Output Schemas ----------------------
|
| 26 |
|
| 27 |
class LocationInput(BaseModel):
|
| 28 |
-
"""Input schema for location
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
location_query: str = Field(
|
| 30 |
description="Location name, address, or description (e.g., 'Malaga, Spain')"
|
| 31 |
)
|
| 32 |
|
| 33 |
|
| 34 |
class LocationOutput(BaseModel):
|
| 35 |
-
"""Output schema for location
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
success: bool
|
| 37 |
coordinates: Optional[Dict[str, float]] = None
|
| 38 |
formatted_address: str = ""
|
|
|
|
| 1 |
"""
|
| 2 |
+
Location Services Tool for Address Resolution and Geocoding.
|
| 3 |
+
|
| 4 |
+
This module provides comprehensive location services including:
|
| 5 |
+
- Address to coordinates conversion (geocoding)
|
| 6 |
+
- Support for various input formats (addresses, coordinates, place names)
|
| 7 |
+
- Distance calculations between coordinates
|
| 8 |
+
- Intelligent parsing of location strings
|
| 9 |
+
|
| 10 |
+
The tool integrates with Nominatim (OpenStreetMap) for geocoding services
|
| 11 |
+
and includes fallback mechanisms for reliable location resolution.
|
| 12 |
+
|
| 13 |
+
Supported input formats:
|
| 14 |
+
- "Málaga, Spain" (city, country)
|
| 15 |
+
- "123 Main St, New York, NY" (full address)
|
| 16 |
+
- "36.7156,-4.4044" (decimal coordinates)
|
| 17 |
+
- "Tarifa Beach" (landmark/place name)
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
>>> tool = LocationTool()
|
| 21 |
+
>>> result = tool.run(LocationInput(location_query="Lisbon, Portugal"))
|
| 22 |
+
>>> print(f"Coordinates: {result.coordinates}")
|
| 23 |
+
|
| 24 |
+
Author: Surf Spot Finder Team
|
| 25 |
+
License: MIT
|
| 26 |
"""
|
| 27 |
|
| 28 |
from typing import Dict, Optional
|
|
|
|
| 47 |
# ---------------------- Input / Output Schemas ----------------------
|
| 48 |
|
| 49 |
class LocationInput(BaseModel):
|
| 50 |
+
"""Input schema for location resolution.
|
| 51 |
+
|
| 52 |
+
Attributes:
|
| 53 |
+
location_query: Location string in any supported format.
|
| 54 |
+
"""
|
| 55 |
location_query: str = Field(
|
| 56 |
description="Location name, address, or description (e.g., 'Malaga, Spain')"
|
| 57 |
)
|
| 58 |
|
| 59 |
|
| 60 |
class LocationOutput(BaseModel):
|
| 61 |
+
"""Output schema for location resolution results.
|
| 62 |
+
|
| 63 |
+
Attributes:
|
| 64 |
+
success: Whether location was successfully resolved.
|
| 65 |
+
coordinates: Dict with 'lat' and 'lon' keys if successful.
|
| 66 |
+
formatted_address: Standardized address string from geocoder.
|
| 67 |
+
error: Error message if resolution failed.
|
| 68 |
+
"""
|
| 69 |
success: bool
|
| 70 |
coordinates: Optional[Dict[str, float]] = None
|
| 71 |
formatted_address: str = ""
|
mcp_server/tools/marine_data_tool.py
CHANGED
|
@@ -1,6 +1,32 @@
|
|
| 1 |
"""
|
| 2 |
-
Open-Meteo
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import requests
|
|
@@ -9,11 +35,37 @@ from typing import Dict, Optional
|
|
| 9 |
|
| 10 |
|
| 11 |
class OpenMeteoMarineAPI:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
BASE_MARINE = "https://marine-api.open-meteo.com/v1/marine"
|
| 13 |
BASE_WEATHER = "https://api.open-meteo.com/v1/forecast"
|
| 14 |
|
| 15 |
def get_current_conditions(self, lat: float, lon: float) -> Dict:
|
| 16 |
-
"""Get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
marine = self._fetch_marine(lat, lon)
|
| 18 |
wind = self._fetch_wind(lat, lon)
|
| 19 |
|
|
@@ -32,7 +84,20 @@ class OpenMeteoMarineAPI:
|
|
| 32 |
}
|
| 33 |
|
| 34 |
def get_forecast_for_hour(self, lat: float, lon: float, target_datetime: datetime) -> Dict:
|
| 35 |
-
"""Get marine forecast for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
marine = self._fetch_marine(lat, lon, forecast_days=3)
|
| 37 |
wind = self._fetch_wind(lat, lon)
|
| 38 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
Open-Meteo Marine Data Tool - Free Reliable Fallback.
|
| 3 |
+
|
| 4 |
+
This module provides marine and weather data from Open-Meteo APIs
|
| 5 |
+
as a free, reliable fallback when premium services are unavailable.
|
| 6 |
+
It's designed to be compatible with the WaveDataOutput schema.
|
| 7 |
+
|
| 8 |
+
Data Sources:
|
| 9 |
+
- Marine API: Wave height, direction, period, swell data
|
| 10 |
+
- Weather API: Wind speed, direction, gusts
|
| 11 |
+
|
| 12 |
+
Features:
|
| 13 |
+
- No API key required
|
| 14 |
+
- High availability and reliability
|
| 15 |
+
- Compatible output format with premium services
|
| 16 |
+
- Supports both current conditions and forecasts
|
| 17 |
+
|
| 18 |
+
Limitations:
|
| 19 |
+
- Lower resolution than premium APIs
|
| 20 |
+
- No water temperature data
|
| 21 |
+
- Reduced forecast accuracy at longer ranges
|
| 22 |
+
|
| 23 |
+
Example:
|
| 24 |
+
>>> api = OpenMeteoMarineAPI()
|
| 25 |
+
>>> conditions = api.get_current_conditions(36.72, -4.42)
|
| 26 |
+
>>> print(f"Wave height: {conditions['wave_height']}m")
|
| 27 |
+
|
| 28 |
+
Author: Surf Spot Finder Team
|
| 29 |
+
License: MIT
|
| 30 |
"""
|
| 31 |
|
| 32 |
import requests
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class OpenMeteoMarineAPI:
|
| 38 |
+
"""Open-Meteo marine data client with fallback capabilities.
|
| 39 |
+
|
| 40 |
+
Provides marine and weather data from Open-Meteo's free APIs.
|
| 41 |
+
Designed as a reliable fallback for premium marine data services.
|
| 42 |
+
|
| 43 |
+
Attributes:
|
| 44 |
+
BASE_MARINE: Open-Meteo marine API endpoint.
|
| 45 |
+
BASE_WEATHER: Open-Meteo weather API endpoint.
|
| 46 |
+
|
| 47 |
+
Example:
|
| 48 |
+
>>> api = OpenMeteoMarineAPI()
|
| 49 |
+
>>> data = api.get_current_conditions(lat=36.72, lon=-4.42)
|
| 50 |
+
>>> forecast = api.get_forecast_for_hour(lat, lon, datetime_obj)
|
| 51 |
+
"""
|
| 52 |
BASE_MARINE = "https://marine-api.open-meteo.com/v1/marine"
|
| 53 |
BASE_WEATHER = "https://api.open-meteo.com/v1/forecast"
|
| 54 |
|
| 55 |
def get_current_conditions(self, lat: float, lon: float) -> Dict:
|
| 56 |
+
"""Get current marine and wind conditions.
|
| 57 |
+
|
| 58 |
+
Fetches current wave, wind, and swell data from Open-Meteo APIs.
|
| 59 |
+
Combines marine and weather data into unified response.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
lat: Latitude in decimal degrees.
|
| 63 |
+
lon: Longitude in decimal degrees.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Dict containing wave_height, wind_speed, directions, etc.
|
| 67 |
+
Compatible with WaveDataOutput schema.
|
| 68 |
+
"""
|
| 69 |
marine = self._fetch_marine(lat, lon)
|
| 70 |
wind = self._fetch_wind(lat, lon)
|
| 71 |
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
def get_forecast_for_hour(self, lat: float, lon: float, target_datetime: datetime) -> Dict:
|
| 87 |
+
"""Get marine forecast for specific datetime.
|
| 88 |
+
|
| 89 |
+
Retrieves forecast data for the specified hour from Open-Meteo.
|
| 90 |
+
Automatically finds the closest available forecast time.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
lat: Latitude in decimal degrees.
|
| 94 |
+
lon: Longitude in decimal degrees.
|
| 95 |
+
target_datetime: Target datetime for forecast data.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
Dict with forecast conditions for the specified time.
|
| 99 |
+
Returns current conditions if forecast unavailable.
|
| 100 |
+
"""
|
| 101 |
marine = self._fetch_marine(lat, lon, forecast_days=3)
|
| 102 |
wind = self._fetch_wind(lat, lon)
|
| 103 |
|
mcp_server/tools/spot_finder_tool.py
CHANGED
|
@@ -1,6 +1,34 @@
|
|
| 1 |
"""
|
| 2 |
-
Main Surf Spot Finder Tool
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import json
|
|
@@ -20,7 +48,14 @@ logger = logging.getLogger(__name__)
|
|
| 20 |
|
| 21 |
|
| 22 |
class SpotFinderInput(BaseModel):
|
| 23 |
-
"""Input schema for spot finder tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
user_location: str = Field(description="User's location (name, address, or coordinates)")
|
| 25 |
max_distance_km: float = Field(default=50, description="Maximum distance to search for spots (km)")
|
| 26 |
top_n: int = Field(default=3, description="Number of top spots to return")
|
|
@@ -28,7 +63,16 @@ class SpotFinderInput(BaseModel):
|
|
| 28 |
|
| 29 |
|
| 30 |
class SpotFinderOutput(BaseModel):
|
| 31 |
-
"""Output schema for spot finder results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
success: bool
|
| 33 |
user_location: Optional[Dict[str, float]] = None
|
| 34 |
spots: List[Dict[str, Any]] = []
|
|
@@ -38,7 +82,34 @@ class SpotFinderOutput(BaseModel):
|
|
| 38 |
|
| 39 |
|
| 40 |
class SurfSpotFinder:
|
| 41 |
-
"""Main orchestration tool for finding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
name = "find_surf_spots"
|
| 44 |
description = "Find and rank the best surf spots near a given location based on current conditions"
|
|
@@ -121,7 +192,20 @@ class SurfSpotFinder:
|
|
| 121 |
|
| 122 |
|
| 123 |
def find_nearby_spots(self, user_lat: float, user_lon: float, max_distance_km: float) -> List[Dict[str, Any]]:
|
| 124 |
-
"""Find spots within specified distance of user location
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
nearby_spots = []
|
| 126 |
user_location = (user_lat, user_lon)
|
| 127 |
|
|
@@ -139,7 +223,18 @@ class SurfSpotFinder:
|
|
| 139 |
return nearby_spots
|
| 140 |
|
| 141 |
async def get_spot_conditions(self, spot: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
| 142 |
-
"""Get current wave conditions for a spot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
try:
|
| 144 |
# Use the Stormglass tool to get wave data
|
| 145 |
from .stormglass_tool import WaveDataInput
|
|
@@ -156,7 +251,20 @@ class SurfSpotFinder:
|
|
| 156 |
return None
|
| 157 |
|
| 158 |
async def evaluate_spot(self, spot: Dict[str, Any], conditions: Dict[str, Any], user_prefs: Dict[str, Any]) -> Dict[str, Any]:
|
| 159 |
-
"""Evaluate a single spot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
evaluation = await self.evaluator.run({
|
| 161 |
"spot": spot,
|
| 162 |
"conditions": conditions,
|
|
@@ -178,7 +286,21 @@ class SurfSpotFinder:
|
|
| 178 |
}
|
| 179 |
|
| 180 |
async def run(self, input_data: SpotFinderInput) -> SpotFinderOutput:
|
| 181 |
-
"""Execute the complete surf spot finding workflow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
try:
|
| 183 |
# Step 1: Resolve user location
|
| 184 |
logger.info(f"Resolving location: {input_data.user_location}")
|
|
@@ -273,8 +395,19 @@ class SurfSpotFinder:
|
|
| 273 |
)
|
| 274 |
|
| 275 |
|
| 276 |
-
def create_spot_finder_tool():
|
| 277 |
-
"""Factory function to create the spot finder tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
tool = SurfSpotFinder()
|
| 279 |
return {
|
| 280 |
"name": tool.name,
|
|
|
|
| 1 |
"""
|
| 2 |
+
Main Surf Spot Finder Tool - Complete Workflow Orchestration.
|
| 3 |
+
|
| 4 |
+
This module provides the primary MCP tool for finding and ranking surf spots.
|
| 5 |
+
It orchestrates the complete workflow from user input to AI-powered recommendations:
|
| 6 |
+
|
| 7 |
+
1. Location resolution (address/coordinates)
|
| 8 |
+
2. Nearby spot discovery (distance filtering)
|
| 9 |
+
3. Real-time condition analysis (wave/wind data)
|
| 10 |
+
4. Multi-factor evaluation and scoring
|
| 11 |
+
5. AI-powered reasoning and explanations
|
| 12 |
+
|
| 13 |
+
The tool integrates multiple data sources:
|
| 14 |
+
- MCP resources for surf spot database
|
| 15 |
+
- Stormglass API for marine conditions
|
| 16 |
+
- Nominatim for geocoding
|
| 17 |
+
- LLM providers for natural language reasoning
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
>>> finder = SurfSpotFinder()
|
| 21 |
+
>>> input_data = SpotFinderInput(
|
| 22 |
+
... user_location="Málaga, Spain",
|
| 23 |
+
... max_distance_km=50,
|
| 24 |
+
... top_n=3,
|
| 25 |
+
... user_preferences={"skill_level": "intermediate"}
|
| 26 |
+
... )
|
| 27 |
+
>>> result = await finder.run(input_data)
|
| 28 |
+
>>> print(f"Found {len(result.spots)} spots")
|
| 29 |
+
|
| 30 |
+
Author: Surf Spot Finder Team
|
| 31 |
+
License: MIT
|
| 32 |
"""
|
| 33 |
|
| 34 |
import json
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
class SpotFinderInput(BaseModel):
|
| 51 |
+
"""Input schema for the surf spot finder tool.
|
| 52 |
+
|
| 53 |
+
Attributes:
|
| 54 |
+
user_location: User's location as address, city name, or coordinates.
|
| 55 |
+
max_distance_km: Maximum search radius in kilometers (default: 50).
|
| 56 |
+
top_n: Number of top-ranked spots to return (default: 3).
|
| 57 |
+
user_preferences: Dict containing skill_level, board_type, etc.
|
| 58 |
+
"""
|
| 59 |
user_location: str = Field(description="User's location (name, address, or coordinates)")
|
| 60 |
max_distance_km: float = Field(default=50, description="Maximum distance to search for spots (km)")
|
| 61 |
top_n: int = Field(default=3, description="Number of top spots to return")
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
class SpotFinderOutput(BaseModel):
|
| 66 |
+
"""Output schema for surf spot finder results.
|
| 67 |
+
|
| 68 |
+
Attributes:
|
| 69 |
+
success: Whether the operation completed successfully.
|
| 70 |
+
user_location: Resolved coordinates as {"lat": float, "lon": float}.
|
| 71 |
+
spots: List of ranked surf spots with scores and conditions.
|
| 72 |
+
ai_summary: Brief AI-generated summary of recommendations.
|
| 73 |
+
ai_reasoning: Detailed AI analysis and explanations.
|
| 74 |
+
error: Error message if success is False.
|
| 75 |
+
"""
|
| 76 |
success: bool
|
| 77 |
user_location: Optional[Dict[str, float]] = None
|
| 78 |
spots: List[Dict[str, Any]] = []
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
class SurfSpotFinder:
|
| 85 |
+
"""Main orchestration tool for finding optimal surf spots.
|
| 86 |
+
|
| 87 |
+
This class coordinates the complete surf recommendation workflow by
|
| 88 |
+
integrating location services, marine data APIs, evaluation algorithms,
|
| 89 |
+
and AI reasoning to provide ranked surf spot recommendations.
|
| 90 |
+
|
| 91 |
+
The workflow:
|
| 92 |
+
1. Resolves user location to coordinates
|
| 93 |
+
2. Filters surf spots by distance
|
| 94 |
+
3. Fetches real-time wave conditions
|
| 95 |
+
4. Evaluates each spot using multi-factor scoring
|
| 96 |
+
5. Generates AI-powered analysis and explanations
|
| 97 |
+
|
| 98 |
+
Attributes:
|
| 99 |
+
name: Tool identifier for MCP registration.
|
| 100 |
+
description: Human-readable tool description.
|
| 101 |
+
location_tool: Service for address/coordinate resolution.
|
| 102 |
+
stormglass_tool: API client for marine condition data.
|
| 103 |
+
evaluator: Surf condition evaluation algorithm.
|
| 104 |
+
llm_agent: AI reasoning and natural language generation.
|
| 105 |
+
spots_db: Cached surf spot database from MCP resources.
|
| 106 |
+
|
| 107 |
+
Example:
|
| 108 |
+
>>> finder = SurfSpotFinder()
|
| 109 |
+
>>> input_data = SpotFinderInput(user_location="Lisbon")
|
| 110 |
+
>>> result = await finder.run(input_data)
|
| 111 |
+
>>> print(f"Best spot: {result.spots[0]['name']}")
|
| 112 |
+
"""
|
| 113 |
|
| 114 |
name = "find_surf_spots"
|
| 115 |
description = "Find and rank the best surf spots near a given location based on current conditions"
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def find_nearby_spots(self, user_lat: float, user_lon: float, max_distance_km: float) -> List[Dict[str, Any]]:
|
| 195 |
+
"""Find surf spots within specified distance of user location.
|
| 196 |
+
|
| 197 |
+
Uses haversine formula to calculate spherical distances between
|
| 198 |
+
user coordinates and each surf spot in the database.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
user_lat: User's latitude in decimal degrees.
|
| 202 |
+
user_lon: User's longitude in decimal degrees.
|
| 203 |
+
max_distance_km: Maximum search radius in kilometers.
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
List of surf spots within radius, sorted by distance.
|
| 207 |
+
Each spot includes original data plus distance_km field.
|
| 208 |
+
"""
|
| 209 |
nearby_spots = []
|
| 210 |
user_location = (user_lat, user_lon)
|
| 211 |
|
|
|
|
| 223 |
return nearby_spots
|
| 224 |
|
| 225 |
async def get_spot_conditions(self, spot: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
| 226 |
+
"""Get current wave conditions for a specific surf spot.
|
| 227 |
+
|
| 228 |
+
Fetches real-time marine data including wave height, direction,
|
| 229 |
+
period, wind speed/direction, and tide information.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
spot: Surf spot dictionary with latitude/longitude.
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
Dict containing current conditions, or None if fetch fails.
|
| 236 |
+
Includes wave_height, wave_direction, wind_speed, etc.
|
| 237 |
+
"""
|
| 238 |
try:
|
| 239 |
# Use the Stormglass tool to get wave data
|
| 240 |
from .stormglass_tool import WaveDataInput
|
|
|
|
| 251 |
return None
|
| 252 |
|
| 253 |
async def evaluate_spot(self, spot: Dict[str, Any], conditions: Dict[str, Any], user_prefs: Dict[str, Any]) -> Dict[str, Any]:
|
| 254 |
+
"""Evaluate a single surf spot using current conditions and user preferences.
|
| 255 |
+
|
| 256 |
+
Applies multi-factor scoring algorithm considering wave conditions,
|
| 257 |
+
wind analysis, swell direction, and skill compatibility.
|
| 258 |
+
|
| 259 |
+
Args:
|
| 260 |
+
spot: Surf spot data including location and characteristics.
|
| 261 |
+
conditions: Current wave/wind conditions from marine APIs.
|
| 262 |
+
user_prefs: User preferences including skill level, board type.
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
Dict containing evaluated spot with score, explanation, and
|
| 266 |
+
breakdown of individual scoring factors.
|
| 267 |
+
"""
|
| 268 |
evaluation = await self.evaluator.run({
|
| 269 |
"spot": spot,
|
| 270 |
"conditions": conditions,
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
async def run(self, input_data: SpotFinderInput) -> SpotFinderOutput:
|
| 289 |
+
"""Execute the complete surf spot finding workflow.
|
| 290 |
+
|
| 291 |
+
This is the main entry point that orchestrates all steps of the
|
| 292 |
+
surf recommendation process from user input to final rankings.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
input_data: User request containing location, preferences, and filters.
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
Complete results including ranked spots, AI analysis, and metadata.
|
| 299 |
+
|
| 300 |
+
Raises:
|
| 301 |
+
No exceptions - all errors are captured in SpotFinderOutput.error
|
| 302 |
+
for graceful degradation and user-friendly error messages.
|
| 303 |
+
"""
|
| 304 |
try:
|
| 305 |
# Step 1: Resolve user location
|
| 306 |
logger.info(f"Resolving location: {input_data.user_location}")
|
|
|
|
| 395 |
)
|
| 396 |
|
| 397 |
|
| 398 |
+
def create_spot_finder_tool() -> Dict[str, Any]:
|
| 399 |
+
"""Factory function to create the surf spot finder tool.
|
| 400 |
+
|
| 401 |
+
Creates and configures the main MCP tool for surf spot recommendations.
|
| 402 |
+
Returns tool specification compatible with MCP protocol.
|
| 403 |
+
|
| 404 |
+
Returns:
|
| 405 |
+
Dict containing tool name, description, schema, and function reference.
|
| 406 |
+
|
| 407 |
+
Example:
|
| 408 |
+
>>> tool = create_spot_finder_tool()
|
| 409 |
+
>>> result = await tool["function"](input_data)
|
| 410 |
+
"""
|
| 411 |
tool = SurfSpotFinder()
|
| 412 |
return {
|
| 413 |
"name": tool.name,
|
mcp_server/tools/stormglass_tool.py
CHANGED
|
@@ -1,7 +1,31 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
|
@@ -18,6 +42,14 @@ from pydantic import BaseModel, Field
|
|
| 18 |
# -------------------------------------------------------
|
| 19 |
|
| 20 |
class WaveDataInput(BaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
lat: float = Field(description="Latitude of the surf spot")
|
| 22 |
lon: float = Field(description="Longitude of the surf spot")
|
| 23 |
target_datetime: Optional[datetime] = Field(
|
|
@@ -27,6 +59,20 @@ class WaveDataInput(BaseModel):
|
|
| 27 |
|
| 28 |
|
| 29 |
class WaveDataOutput(BaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
wave_height: float
|
| 31 |
wave_direction: float
|
| 32 |
wave_period: float
|
|
|
|
| 1 |
"""
|
| 2 |
+
Stormglass Marine Data Tool - Premium Wave and Wind Conditions.
|
| 3 |
+
|
| 4 |
+
This module provides access to high-quality marine data from the Stormglass API,
|
| 5 |
+
with intelligent fallback to Open-Meteo for reliability. It fetches real-time
|
| 6 |
+
and forecast data including waves, wind, swell, and water temperature.
|
| 7 |
+
|
| 8 |
+
Data Sources (Priority Order):
|
| 9 |
+
1. Stormglass API: Premium accuracy, requires API key
|
| 10 |
+
2. Open-Meteo Marine: Free fallback, always available
|
| 11 |
+
|
| 12 |
+
Supported Parameters:
|
| 13 |
+
- Wave height, direction, and period
|
| 14 |
+
- Wind speed, direction, and gusts
|
| 15 |
+
- Swell direction and characteristics
|
| 16 |
+
- Water temperature (Stormglass only)
|
| 17 |
+
- Forecast data up to 10 days
|
| 18 |
+
|
| 19 |
+
The tool automatically handles API failures, rate limits, and
|
| 20 |
+
missing API keys by falling back to the free Open-Meteo service.
|
| 21 |
+
|
| 22 |
+
Example:
|
| 23 |
+
>>> tool = create_stormglass_tool()["function"]
|
| 24 |
+
>>> result = tool(WaveDataInput(lat=36.72, lon=-4.42))
|
| 25 |
+
>>> print(f"Wave height: {result.wave_height}m")
|
| 26 |
+
|
| 27 |
+
Author: Surf Spot Finder Team
|
| 28 |
+
License: MIT
|
| 29 |
"""
|
| 30 |
|
| 31 |
import os
|
|
|
|
| 42 |
# -------------------------------------------------------
|
| 43 |
|
| 44 |
class WaveDataInput(BaseModel):
|
| 45 |
+
"""Input schema for wave data requests.
|
| 46 |
+
|
| 47 |
+
Attributes:
|
| 48 |
+
lat: Latitude in decimal degrees (-90 to 90).
|
| 49 |
+
lon: Longitude in decimal degrees (-180 to 180).
|
| 50 |
+
target_datetime: Optional future datetime for forecast data.
|
| 51 |
+
If None, returns current conditions.
|
| 52 |
+
"""
|
| 53 |
lat: float = Field(description="Latitude of the surf spot")
|
| 54 |
lon: float = Field(description="Longitude of the surf spot")
|
| 55 |
target_datetime: Optional[datetime] = Field(
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
class WaveDataOutput(BaseModel):
|
| 62 |
+
"""Output schema for marine condition data.
|
| 63 |
+
|
| 64 |
+
Attributes:
|
| 65 |
+
wave_height: Significant wave height in meters.
|
| 66 |
+
wave_direction: Wave direction in degrees (0-360).
|
| 67 |
+
wave_period: Wave period in seconds.
|
| 68 |
+
wind_speed: Wind speed in meters per second.
|
| 69 |
+
wind_direction: Wind direction in degrees (0-360).
|
| 70 |
+
swell_direction: Primary swell direction in degrees.
|
| 71 |
+
wind_gust: Maximum wind gust speed in m/s.
|
| 72 |
+
water_temperature: Water temperature in Celsius.
|
| 73 |
+
timestamp: Data timestamp in ISO format.
|
| 74 |
+
forecast_target: Target forecast time if applicable.
|
| 75 |
+
"""
|
| 76 |
wave_height: float
|
| 77 |
wave_direction: float
|
| 78 |
wave_period: float
|
mcp_server/tools/surf_eval_tool.py
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import Dict, Any, List, Tuple
|
| 3 |
import math
|
|
@@ -6,16 +37,57 @@ import math
|
|
| 6 |
class SurfEvaluatorTool:
|
| 7 |
"""
|
| 8 |
Advanced surf evaluation tool with multi-factor scoring algorithm.
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
class SurfEvalInput(BaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
spot: Dict[str, Any]
|
| 14 |
conditions: Dict[str, Any]
|
| 15 |
prefs: Dict[str, Any] = {}
|
| 16 |
|
| 17 |
def is_direction_in_range(self, direction: float, direction_range: List[float]) -> bool:
|
| 18 |
-
"""Check if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
if len(direction_range) != 2:
|
| 20 |
return False
|
| 21 |
|
|
@@ -28,7 +100,18 @@ class SurfEvaluatorTool:
|
|
| 28 |
return start <= direction <= end
|
| 29 |
|
| 30 |
def calculate_direction_score(self, direction: float, optimal_range: List[float]) -> float:
|
| 31 |
-
"""Calculate score based on
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
if not optimal_range or len(optimal_range) != 2:
|
| 33 |
return 0.5 # Neutral score if no preference
|
| 34 |
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Surf Evaluation Tool - Multi-Factor Scoring Algorithm.
|
| 3 |
+
|
| 4 |
+
This module implements a sophisticated surf condition evaluation system
|
| 5 |
+
that scores surf spots based on multiple environmental factors and user
|
| 6 |
+
preferences. The algorithm considers wave conditions, wind patterns,
|
| 7 |
+
swell direction, and skill compatibility.
|
| 8 |
+
|
| 9 |
+
Scoring Components (weighted):
|
| 10 |
+
- Wave Conditions (35%): Height matching and safety
|
| 11 |
+
- Wind Analysis (25%): Speed and direction optimization
|
| 12 |
+
- Swell Direction (25%): Angular matching to optimal window
|
| 13 |
+
- Skill Compatibility (15%): Break type vs experience level
|
| 14 |
+
|
| 15 |
+
The scoring system uses normalized 0-100 scales with progressive
|
| 16 |
+
curves that reward optimal conditions while penalizing dangerous
|
| 17 |
+
or suboptimal scenarios.
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
>>> evaluator = SurfEvaluatorTool()
|
| 21 |
+
>>> result = await evaluator.run({
|
| 22 |
+
... "spot": surf_spot_data,
|
| 23 |
+
... "conditions": current_wave_data,
|
| 24 |
+
... "prefs": {"skill_level": "intermediate"}
|
| 25 |
+
... })
|
| 26 |
+
>>> print(f"Score: {result['score']}/100")
|
| 27 |
+
|
| 28 |
+
Author: Surf Spot Finder Team
|
| 29 |
+
License: MIT
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
from pydantic import BaseModel
|
| 33 |
from typing import Dict, Any, List, Tuple
|
| 34 |
import math
|
|
|
|
| 37 |
class SurfEvaluatorTool:
|
| 38 |
"""
|
| 39 |
Advanced surf evaluation tool with multi-factor scoring algorithm.
|
| 40 |
+
|
| 41 |
+
This tool implements a comprehensive evaluation system that analyzes
|
| 42 |
+
surf conditions across multiple dimensions to produce a single score
|
| 43 |
+
representing the quality and suitability of surfing conditions.
|
| 44 |
+
|
| 45 |
+
The algorithm weighs different factors based on their importance:
|
| 46 |
+
- Wave conditions: Safety and optimal height ranges
|
| 47 |
+
- Wind patterns: Offshore vs onshore preferences
|
| 48 |
+
- Swell direction: Alignment with spot's optimal angles
|
| 49 |
+
- User skill level: Appropriate difficulty matching
|
| 50 |
+
|
| 51 |
+
All scores are normalized to 0-100 scale for consistency.
|
| 52 |
+
|
| 53 |
+
Attributes:
|
| 54 |
+
name: Tool identifier for MCP registration.
|
| 55 |
+
description: Human-readable tool description.
|
| 56 |
+
|
| 57 |
+
Example:
|
| 58 |
+
>>> evaluator = SurfEvaluatorTool()
|
| 59 |
+
>>> input_data = SurfEvaluatorTool.SurfEvalInput(
|
| 60 |
+
... spot=spot_data,
|
| 61 |
+
... conditions=wave_conditions,
|
| 62 |
+
... prefs={"skill_level": "beginner"}
|
| 63 |
+
... )
|
| 64 |
+
>>> result = await evaluator.run(input_data.dict())
|
| 65 |
"""
|
| 66 |
|
| 67 |
class SurfEvalInput(BaseModel):
|
| 68 |
+
"""Input schema for surf evaluation.
|
| 69 |
+
|
| 70 |
+
Attributes:
|
| 71 |
+
spot: Surf spot data including location and characteristics.
|
| 72 |
+
conditions: Current wave/wind conditions from marine APIs.
|
| 73 |
+
prefs: User preferences including skill level and board type.
|
| 74 |
+
"""
|
| 75 |
spot: Dict[str, Any]
|
| 76 |
conditions: Dict[str, Any]
|
| 77 |
prefs: Dict[str, Any] = {}
|
| 78 |
|
| 79 |
def is_direction_in_range(self, direction: float, direction_range: List[float]) -> bool:
|
| 80 |
+
"""Check if direction falls within optimal range for the surf spot.
|
| 81 |
+
|
| 82 |
+
Handles both normal ranges and those crossing 0° (e.g., 315-45°).
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
direction: Current direction in degrees (0-360).
|
| 86 |
+
direction_range: [start, end] optimal range in degrees.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
True if direction is within the optimal range.
|
| 90 |
+
"""
|
| 91 |
if len(direction_range) != 2:
|
| 92 |
return False
|
| 93 |
|
|
|
|
| 100 |
return start <= direction <= end
|
| 101 |
|
| 102 |
def calculate_direction_score(self, direction: float, optimal_range: List[float]) -> float:
|
| 103 |
+
"""Calculate score based on direction proximity to optimal range.
|
| 104 |
+
|
| 105 |
+
Uses progressive scoring where perfect alignment = 1.0,
|
| 106 |
+
and score decreases with angular distance from optimal range.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
direction: Current direction in degrees.
|
| 110 |
+
optimal_range: [start, end] optimal range in degrees.
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
Score between 0.0-1.0 based on direction quality.
|
| 114 |
+
"""
|
| 115 |
if not optimal_range or len(optimal_range) != 2:
|
| 116 |
return 0.5 # Neutral score if no preference
|
| 117 |
|