search_activities_tool
Find Strava activities using filters like type, date range, distance, or search terms to locate specific workouts in your fitness history.
Instructions
Search activities with optional filters. Note: Name/description search is client-side, so a reasonable limit is recommended.
Args: query: Search term to match in activity name (case-insensitive, partial match) activity_type: Filter by activity type (e.g., "Run", "Ride", "Walk", "Hike") after: ISO 8601 date string (e.g., "2025-01-01") - activities after this date before: ISO 8601 date string (e.g., "2026-01-01") - activities before this date min_distance: Minimum distance in meters max_distance: Maximum distance in meters limit: Maximum number of activities to fetch from API (default 50)
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | ||
| activity_type | No | ||
| after | No | ||
| before | No | ||
| min_distance | No | ||
| max_distance | No | ||
| limit | No |
Implementation Reference
- server.py:54-91 (handler)MCP tool handler for search_activities_tool. Decorated with @mcp.tool(), this function accepts query parameters (query, activity_type, after, before, min_distance, max_distance, limit), applies MAX_LIMIT clamping, calls the search_activities service function, and returns a list of dictionaries representing activity summaries.
@mcp.tool() def search_activities_tool( query: Optional[str] = None, activity_type: Optional[str] = None, after: Optional[str] = None, before: Optional[str] = None, min_distance: Optional[float] = None, max_distance: Optional[float] = None, limit: int = 50, ) -> list[dict]: """ Search activities with optional filters. Note: Name/description search is client-side, so a reasonable limit is recommended. Args: query: Search term to match in activity name (case-insensitive, partial match) activity_type: Filter by activity type (e.g., "Run", "Ride", "Walk", "Hike") after: ISO 8601 date string (e.g., "2025-01-01") - activities after this date before: ISO 8601 date string (e.g., "2026-01-01") - activities before this date min_distance: Minimum distance in meters max_distance: Maximum distance in meters limit: Maximum number of activities to fetch from API (default 50) """ if limit > MAX_LIMIT: limit = MAX_LIMIT client = get_client() activities = search_activities( client, query=query, activity_type=activity_type, after=after, before=before, min_distance=min_distance, max_distance=max_distance, limit=limit, ) return [activity.to_dict() for activity in activities] - Core service function that implements the search logic. Parses ISO 8601 date strings, fetches activities from Strava API using client.get_activities(), then applies client-side filters for query (case-insensitive name matching), activity_type, and distance ranges. Returns a list of ActivitySummary objects.
def search_activities( client: Client, query: Optional[str] = None, activity_type: Optional[str] = None, after: Optional[str] = None, before: Optional[str] = None, min_distance: Optional[float] = None, max_distance: Optional[float] = None, limit: int = 50, ) -> list[ActivitySummary]: """Search activities with optional filters.""" # Parse date strings if provided after_date = None before_date = None if after: try: after_date = datetime.datetime.fromisoformat(after.replace("Z", "+00:00")) except ValueError: sys.stderr.write(f"Warning: Invalid 'after' date format: {after}\n") if before: try: before_date = datetime.datetime.fromisoformat(before.replace("Z", "+00:00")) except ValueError: sys.stderr.write(f"Warning: Invalid 'before' date format: {before}\n") # Fetch activities with date filters activities = client.get_activities( before=before_date, after=after_date, limit=limit ) result = [] query_lower = query.lower() if query else None type_lower = activity_type.lower() if activity_type else None for activity in activities: # Handle moving_time safely moving_time = ( getattr(activity.moving_time, "seconds", 0) if activity.moving_time else 0 ) # Get activity details activity_name = activity.name or "" activity_type_str = str(activity.type) activity_distance = float(activity.distance) if activity.distance else 0.0 # Apply filters if query_lower and query_lower not in activity_name.lower(): continue if type_lower and type_lower not in activity_type_str.lower(): continue if min_distance is not None and activity_distance < min_distance: continue if max_distance is not None and activity_distance > max_distance: continue summary = ActivitySummary( id=activity.id or 0, name=activity_name, type=activity_type_str, start_date=activity.start_date.isoformat() if activity.start_date else None, distance=activity_distance, moving_time=moving_time, total_elevation_gain=float(activity.total_elevation_gain) if activity.total_elevation_gain else 0.0, ) result.append(summary) return result - strava_mcp/models.py:41-57 (schema)Dataclass schema defining the ActivitySummary model with fields: id, name, type, start_date, distance, moving_time, total_elevation_gain, average_speed, and max_speed. Includes a to_dict() method for serialization.
@dataclass class ActivitySummary: """Summary of a Strava activity.""" id: int name: str type: str start_date: Optional[str] distance: float moving_time: int total_elevation_gain: float average_speed: float = 0.0 max_speed: float = 0.0 def to_dict(self) -> dict: """Convert to dictionary for serialization.""" return asdict(self) - server.py:21-21 (helper)Constant MAX_LIMIT = 200 used to clamp the limit parameter in search_activities_tool to prevent excessive API requests.
MAX_LIMIT = 200 - tests/test_limits.py:41-57 (registration)Test case that verifies the search_activities_tool properly clamps excessive limit values to MAX_LIMIT. Shows how the tool is imported and accessed via the .fn attribute.
@patch("server.get_client") @patch("server.search_activities") def test_search_activities_tool_limit_clamping(mock_search_activities, mock_get_client): # Setup mock_client = MagicMock() mock_get_client.return_value = mock_client mock_search_activities.return_value = [] # Test with limit > MAX_LIMIT excessive_limit = MAX_LIMIT + 100 search_activities_tool.fn(limit=excessive_limit) # Verify search_activities was called with MAX_LIMIT (among other args) # Check that limit=MAX_LIMIT is in kwargs or args call_args = mock_search_activities.call_args assert call_args.kwargs["limit"] == MAX_LIMIT