D3MI4N commited on
Commit
23a9367
·
1 Parent(s): 5796406

docs: add comprehensive docstrings to all Python files

Browse files
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
- """Main function to find surf spots"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- import os, requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # # MCP Server URL for deployed mode
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 (for local development)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (Modal or FastAPI)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  payload = {
67
- "location": user_location, # Modal endpoint expects "location"
68
  "max_distance": max_distance_km,
69
  "num_spots": top_n,
70
  "preferences": prefs or {}
71
  }
72
  try:
73
- # Try Modal endpoint first
74
- if MODAL_URL:
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 - prioritizes Modal serverless deployment for production"""
103
 
104
- # Priority 1: Modal serverless deployment (production)
105
- if MODAL_URL:
106
- print(f"🚀 Using Modal serverless deployment: {MODAL_URL}")
107
- return find_best_spots_http(user_location, max_distance_km, top_n, prefs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
- # Priority 2: Direct local mode (development)
110
- elif DIRECT_MODE:
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 3: HTTP fallback
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 configures the Model Context Protocol server and exposes
5
- surf spots data as MCP resources.
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Legacy tools listing (keep for backwards compatibility if needed)
71
- # This was in the old file - we can remove it later
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
- Placeholder implementation:
9
- Computes haversine distance in km.
10
- Replace with your real version when ready.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Haversine formula (simple but OK)
23
- R = 6371 # earth radius in km
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 Finder
3
- Provides intelligent reasoning and natural language explanations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- MCP Tool for Location Services
3
- Handles geocoding, reverse geocoding, and location parsing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 tool"""
 
 
 
 
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 tool"""
 
 
 
 
 
 
 
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 fallback marine data for Surf Spot Finder
3
- Lightweight version compatible with WaveDataOutput schema
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 real marine + wind data for current time."""
 
 
 
 
 
 
 
 
 
 
 
 
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 a specific hour."""
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Orchestrates the complete workflow: location -> spots -> conditions -> evaluation -> ranking
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 the best surf spots"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 with current conditions"""
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- MCP Tool Stormglass Marine Data
3
- Fetch real marine conditions (wave, swell, wind) with forecast support.
4
- Includes graceful fallback to Open-Meteo.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Evaluates surf spots based on wave conditions, wind, and user preferences.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 wind/swell direction falls within optimal range for the spot."""
 
 
 
 
 
 
 
 
 
 
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 how close direction is to optimal range."""
 
 
 
 
 
 
 
 
 
 
 
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