#
tokens: 47738/50000 31/31 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .env.example
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── setup-auth.ts
├── server.json
├── src
│   ├── formatters.ts
│   ├── server.ts
│   ├── stravaClient.ts
│   └── tools
│       ├── exploreSegments.ts
│       ├── exportRouteGpx.ts
│       ├── exportRouteTcx.ts
│       ├── formatWorkoutFile.ts
│       ├── getActivityDetails.ts
│       ├── getActivityLaps.ts
│       ├── getActivityStreams.ts
│       ├── getAllActivities.ts
│       ├── getAthleteProfile.ts
│       ├── getAthleteStats.ts
│       ├── getAthleteZones.ts
│       ├── getRecentActivities.ts
│       ├── getRoute.ts
│       ├── getSegment.ts
│       ├── getSegmentEffort.ts
│       ├── listAthleteClubs.ts
│       ├── listAthleteRoutes.ts
│       ├── listSegmentEfforts.ts
│       ├── listStarredSegments.ts
│       └── starSegment.ts
├── test-strava-api.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/

# Build output
dist/

# Environment variables
.env

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Cursor IDE files
.cursor/
*.mdc
cursor_rules.json 
.mcpregistry_registry_token
.mcpregistry_github_token

```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
# Strava API Access Token
# Get yours from https://www.strava.com/settings/api
STRAVA_ACCESS_TOKEN=YOUR_STRAVA_ACCESS_TOKEN_HERE 
# Optional: Define a path for saving exported route files (GPX/TCX)
# Ensure this directory exists and the server process has write permissions.
# Example: ROUTE_EXPORT_PATH=/Users/your_username/strava-exports
ROUTE_EXPORT_PATH=./strava-exports
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/r-huijts-strava-mcp-badge.png)](https://mseep.ai/app/r-huijts-strava-mcp)

# Strava MCP Server

This project implements a Model Context Protocol (MCP) server in TypeScript that acts as a bridge to the Strava API. It exposes Strava data and functionalities as "tools" that Large Language Models (LLMs) can utilize through the MCP standard.

<a href="https://glama.ai/mcp/servers/@r-huijts/strava-mcp">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@r-huijts/strava-mcp/badge" alt="Strava Server MCP server" />
</a>

## Features

- 🏃 Access recent activities, profile, and stats.
- 📊 Fetch detailed activity streams (power, heart rate, cadence, etc.).
- 🗺️ Explore, view, star, and manage segments.
- ⏱️ View detailed activity and segment effort information.
- 📍 List and view details of saved routes.
- 💾 Export routes in GPX or TCX format to the local filesystem.
- 🤖 AI-friendly JSON responses via MCP.
- 🔧 Uses Strava API V3.

## Natural Language Interaction Examples

Ask your AI assistant questions like these to interact with your Strava data:

**Recent Activity & Profile:**
* "Show me my recent Strava activities."
* "What were my last 3 rides?"
* "Get my Strava profile information."
* "What's my Strava username?"

**Activity Streams & Data:**
* "Get the heart rate data from my morning run yesterday."
* "Show me the power data from my last ride."
* "What was my cadence profile for my weekend century ride?"
* "Get all stream data for my Thursday evening workout."
* "Show me the elevation profile for my Mt. Diablo climb."

**Stats:**
* "What are my running stats for this year on Strava?"
* "How far have I cycled in total?"
* "Show me my all-time swim totals."

**Specific Activities:**
* "Give me the details for my last run."
* "What was the average power for my interval training on Tuesday?"
* "Did I use my Trek bike for my commute yesterday?"

**Clubs:**
* "What Strava clubs am I in?"
* "List the clubs I've joined."

**Segments:**
* "List the segments I starred near Boulder, Colorado."
* "Show my favorite segments."
* "Get details for the 'Alpe du Zwift' segment."
* "Are there any good running segments near Golden Gate Park?"
* "Find challenging climbs near Boulders Flagstaff Mountain."
* "Star the 'Flagstaff Road Climb' segment for me."
* "Unstar the 'Lefthand Canyon' segment."

**Segment Efforts:**
* "Show my efforts on the 'Sunshine Canyon' segment this month."
* "List my attempts on Box Hill between January and June this year."
* "Get the details for my personal record on Alpe d'Huez."

**Routes:**
* "List my saved Strava routes."
* "Show the second page of my routes."
* "What is the elevation gain for my Boulder Loop route?"
* "Get the description for my 'Boulder Loop' route."
* "Export my 'Boulder Loop' route as a GPX file."
* "Save my Sunday morning route as a TCX file."

## Advanced Prompt Example

Here's an example of a more advanced prompt to create a professional cycling coach analysis of your Strava activities:

```
You are Tom Verhaegen, elite cycling coach and mentor to world champion Mathieu van der Poel. Analyze my most recent Strava activity. Provide a thorough, data-driven assessment of the ride, combining both quantitative insights and textual interpretation.

Begin your report with a written summary that highlights key findings and context. Then, bring the raw numbers to life: build an interactive, visually striking dashboard using HTML, CSS, and JavaScript. Use bold, high-contrast colors and intuitive, insightful chart types that best suit each metric (e.g., heart rate, power, cadence, elevation).

Embed clear coaching feedback and personalized training recommendations directly within the visualization. These should be practical, actionable, and grounded solely in the data provided—no assumptions or fabrications.

As a bonus, sprinkle in motivational quotes and cheeky commentary from Mathieu van der Poel himself—he's been watching my rides with one eyebrow raised and a smirk of both concern and amusement.

Goal: Deliver a professional-grade performance analysis that looks and feels like it came straight from the inner circle of world-class cycling.
```

This prompt creates a personalized analysis of your most recent Strava activity, complete with professional coaching feedback and a custom visualization dashboard.

## ⚠️ Important Setup Sequence

For successful integration with Claude, follow these steps in exact order:

1. Install the server and its dependencies
2. Configure the server in Claude's configuration
3. Complete the Strava authentication flow
4. Restart Claude to ensure proper environment variable loading

Skipping steps or performing them out of order may result in environment variables not being properly read by Claude.

## Installation & Setup

1. **Prerequisites:**
   - Node.js (v18 or later recommended)
   - npm (usually comes with Node.js)
   - A Strava Account

### 1. From Source

1. **Clone Repository:**
   ```bash
   git clone https://github.com/r-huijts/strava-mcp.git
   cd strava-mcp
   ```

2. **Install Dependencies:**
   ```bash
   npm install
   ```
3. **Build the Project:**
   ```bash
   npm run build
   ```

### 2. Configure Claude Desktop

Update your Claude configuration file:

```json
{
  "mcpServers": {
    "strava-mcp-local": {
      "command": "node",
      "args": [
        "/absolute/path/to/your/strava-mcp/dist/server.js"
      ]
      // Environment variables are read from the .env file by the server
    }
  }
}
```

Make sure to replace `/absolute/path/to/your/strava-mcp/` with the actual path to your installation.

### 3. Strava Authentication Setup

The `setup-auth.ts` script makes it easy to set up authentication with the Strava API. Follow these steps carefully:

#### Create a Strava API Application

1. Go to [https://www.strava.com/settings/api](https://www.strava.com/settings/api)
2. Create a new application:
   - Enter your application details (name, website, description)
   - Important: Set "Authorization Callback Domain" to `localhost`
   - Note down your Client ID and Client Secret

#### Run the Setup Script

```bash
# In your strava-mcp directory
npx tsx scripts/setup-auth.ts
```

Follow the prompts to complete the authentication flow (detailed instructions in the Authentication section below).

### 4. Restart Claude

After completing all the above steps, restart Claude Desktop for the changes to take effect. This ensures that:
- The new configuration is loaded
- The environment variables are properly read
- The Strava MCP server is properly initialized

## 🔑 Environment Variables

| Variable | Description |
|----------|-------------|
| STRAVA_CLIENT_ID | Your Strava Application Client ID (required) |
| STRAVA_CLIENT_SECRET | Your Strava Application Client Secret (required) |
| STRAVA_ACCESS_TOKEN | Your Strava API access token (generated during setup) |
| STRAVA_REFRESH_TOKEN | Your Strava API refresh token (generated during setup) |
| ROUTE_EXPORT_PATH | Absolute path for saving exported route files (optional) |

## Token Handling

This server implements automatic token refreshing. When the initial access token expires (typically after 6 hours), the server will automatically use the refresh token stored in `.env` to obtain a new access token and refresh token. These new tokens are then updated in both the running process and the `.env` file, ensuring continuous operation.

You only need to run the `scripts/setup-auth.ts` script once for the initial setup.

## Configure Export Path (Optional)

If you intend to use the `export-route-gpx` or `export-route-tcx` tools, you need to specify a directory for saving exported files.

Edit your `.env` file and add/update the `ROUTE_EXPORT_PATH` variable:
```dotenv
# Optional: Define an *absolute* path for saving exported route files (GPX/TCX)
# Ensure this directory exists and the server process has write permissions.
# Example: ROUTE_EXPORT_PATH=/Users/your_username/strava-exports
ROUTE_EXPORT_PATH=
```

Replace the placeholder with the **absolute path** to your desired export directory. Ensure the directory exists and the server has permission to write to it.

## API Reference

The server exposes the following MCP tools:

---

### `get-recent-activities`

Fetches the authenticated user's recent activities.

- **When to use:** When the user asks about their recent workouts, activities, runs, rides, etc.
- **Parameters:**
  - `perPage` (optional):
    - Type: `number`
    - Description: Number of activities to retrieve.
    - Default: 30
- **Output:** Formatted text list of recent activities (Name, ID, Distance, Date).
- **Errors:** Missing/invalid token, Strava API errors.

---

### `get-athlete-profile`

Fetches the profile information for the authenticated athlete.

- **When to use:** When the user asks for their profile details, username, location, weight, premium status, etc.
- **Parameters:** None
- **Output:** Formatted text string with profile details.
- **Errors:** Missing/invalid token, Strava API errors.

---

### `get-athlete-stats`

Fetches activity statistics (recent, YTD, all-time) for the authenticated athlete.

- **When to use:** When the user asks for their overall statistics, totals for runs/rides/swims, personal records (longest ride, biggest climb).
- **Parameters:** None
- **Output:** Formatted text summary of stats, respecting user's measurement preference.
- **Errors:** Missing/invalid token, Strava API errors.

---

### `get-activity-details`

Fetches detailed information about a specific activity using its ID.

- **When to use:** When the user asks for details about a *specific* activity identified by its ID.
- **Parameters:**
  - `activityId` (required):
    - Type: `number`
    - Description: The unique identifier of the activity.
- **Output:** Formatted text string with detailed activity information (type, date, distance, time, speed, HR, power, gear, etc.), respecting user's measurement preference.
- **Errors:** Missing/invalid token, Invalid `activityId`, Strava API errors.

---

### `list-athlete-clubs`

Lists the clubs the authenticated athlete is a member of.

- **When to use:** When the user asks about the clubs they have joined.
- **Parameters:** None
- **Output:** Formatted text list of clubs (Name, ID, Sport, Members, Location).
- **Errors:** Missing/invalid token, Strava API errors.

---

### `list-starred-segments`

Lists the segments starred by the authenticated athlete.

- **When to use:** When the user asks about their starred or favorite segments.
- **Parameters:** None
- **Output:** Formatted text list of starred segments (Name, ID, Type, Distance, Grade, Location).
- **Errors:** Missing/invalid token, Strava API errors.

---

### `get-segment`

Fetches detailed information about a specific segment using its ID.

- **When to use:** When the user asks for details about a *specific* segment identified by its ID.
- **Parameters:**
  - `segmentId` (required):
    - Type: `number`
    - Description: The unique identifier of the segment.
- **Output:** Formatted text string with detailed segment information (distance, grade, elevation, location, stars, efforts, etc.), respecting user's measurement preference.
- **Errors:** Missing/invalid token, Invalid `segmentId`, Strava API errors.

---

### `explore-segments`

Searches for popular segments within a given geographical area (bounding box).

- **When to use:** When the user wants to find or discover segments in a specific geographic area, optionally filtering by activity type or climb category.
- **Parameters:**
  - `bounds` (required):
    - Type: `string`
    - Description: Comma-separated: `south_west_lat,south_west_lng,north_east_lat,north_east_lng`.
  - `activityType` (optional):
    - Type: `string` (`"running"` or `"riding"`)
    - Description: Filter by activity type.
  - `minCat` (optional):
    - Type: `number` (0-5)
    - Description: Minimum climb category. Requires `activityType: 'riding'`.
  - `maxCat` (optional):
    - Type: `number` (0-5)
    - Description: Maximum climb category. Requires `activityType: 'riding'`.
- **Output:** Formatted text list of found segments (Name, ID, Climb Cat, Distance, Grade, Elevation).
- **Errors:** Missing/invalid token, Invalid `bounds` format, Invalid filter combination, Strava API errors.

---

### `star-segment`

Stars or unstars a specific segment for the authenticated athlete.

- **When to use:** When the user explicitly asks to star, favorite, unstar, or unfavorite a specific segment identified by its ID.
- **Parameters:**
  - `segmentId` (required):
    - Type: `number`
    - Description: The unique identifier of the segment.
  - `starred` (required):
    - Type: `boolean`
    - Description: `true` to star, `false` to unstar.
- **Output:** Success message confirming the action and the segment's new starred status.
- **Errors:** Missing/invalid token, Invalid `segmentId`, Strava API errors (e.g., segment not found, rate limit).

- **Notes:**
  - Requires `profile:write` scope for star-ing and unstar-ing segments

---

### `get-segment-effort`

Fetches detailed information about a specific segment effort using its ID.

- **When to use:** When the user asks for details about a *specific* segment effort identified by its ID.
- **Parameters:**
  - `effortId` (required):
    - Type: `number`
    - Description: The unique identifier of the segment effort.
- **Output:** Formatted text string with detailed effort information (segment name, activity ID, time, distance, HR, power, rank, etc.).
- **Errors:** Missing/invalid token, Invalid `effortId`, Strava API errors.

---

### `list-segment-efforts`

Lists the authenticated athlete's efforts on a given segment, optionally filtered by date.

- **When to use:** When the user asks to list their efforts or attempts on a specific segment, possibly within a date range.
- **Parameters:**
  - `segmentId` (required):
    - Type: `number`
    - Description: The ID of the segment.
  - `startDateLocal` (optional):
    - Type: `string` (ISO 8601 format)
    - Description: Filter efforts starting after this date-time.
  - `endDateLocal` (optional):
    - Type: `string` (ISO 8601 format)
    - Description: Filter efforts ending before this date-time.
  - `perPage` (optional):
    - Type: `number`
    - Description: Number of results per page.
    - Default: 30
- **Output:** Formatted text list of matching segment efforts.
- **Errors:** Missing/invalid token, Invalid `segmentId`, Invalid date format, Strava API errors.

---

### `list-athlete-routes`

Lists the routes created by the authenticated athlete.

- **When to use:** When the user asks to see the routes they have created or saved.
- **Parameters:**
  - `page` (optional):
    - Type: `number`
    - Description: Page number for pagination.
  - `perPage` (optional):
    - Type: `number`
    - Description: Number of routes per page.
    - Default: 30
- **Output:** Formatted text list of routes (Name, ID, Type, Distance, Elevation, Date).
- **Errors:** Missing/invalid token, Strava API errors.

---

### `get-route`

Fetches detailed information for a specific route using its ID.

- **When to use:** When the user asks for details about a *specific* route identified by its ID.
- **Parameters:**
  - `routeId` (required):
    - Type: `number`
    - Description: The unique identifier of the route.
- **Output:** Formatted text string with route details (Name, ID, Type, Distance, Elevation, Est. Time, Description, Segment Count).
- **Errors:** Missing/invalid token, Invalid `routeId`, Strava API errors.

---

### `export-route-gpx`

Exports a specific route in GPX format and saves it locally.

- **When to use:** When the user explicitly asks to export or save a specific route as a GPX file.
- **Prerequisite:** The `ROUTE_EXPORT_PATH` environment variable must be correctly configured on the server.
- **Parameters:**
  - `routeId` (required):
    - Type: `number`
    - Description: The unique identifier of the route.
- **Output:** Success message indicating the save location, or an error message.
- **Errors:** Missing/invalid token, Missing/invalid `ROUTE_EXPORT_PATH`, File system errors (permissions, disk space), Invalid `routeId`, Strava API errors.

---

### `export-route-tcx`

Exports a specific route in TCX format and saves it locally.

- **When to use:** When the user explicitly asks to export or save a specific route as a TCX file.
- **Prerequisite:** The `ROUTE_EXPORT_PATH` environment variable must be correctly configured on the server.
- **Parameters:**
  - `routeId` (required):
    - Type: `number`
    - Description: The unique identifier of the route.
- **Output:** Success message indicating the save location, or an error message.
- **Errors:** Missing/invalid token, Missing/invalid `ROUTE_EXPORT_PATH`, File system errors (permissions, disk space), Invalid `routeId`, Strava API errors.

---

### `get-activity-streams`

Retrieves detailed time-series data streams from a Strava activity, perfect for analyzing workout metrics, visualizing routes, or performing detailed activity analysis.

- **When to use:** When you need detailed time-series data from an activity for:
  - Analyzing workout intensity through heart rate zones
  - Calculating power metrics for cycling activities
  - Visualizing route data using GPS coordinates
  - Analyzing pace and elevation changes
  - Detailed segment analysis

- **Parameters:**
  - `id` (required):
    - Type: `number | string`
    - Description: The Strava activity identifier to fetch streams for
  - `types` (optional):
    - Type: `array`
    - Default: `['time', 'distance', 'heartrate', 'cadence', 'watts']`
    - Available types:
      - `time`: Time in seconds from start
      - `distance`: Distance in meters from start
      - `latlng`: Array of [latitude, longitude] pairs
      - `altitude`: Elevation in meters
      - `velocity_smooth`: Smoothed speed in meters/second
      - `heartrate`: Heart rate in beats per minute
      - `cadence`: Cadence in revolutions per minute
      - `watts`: Power output in watts
      - `temp`: Temperature in Celsius
      - `moving`: Boolean indicating if moving
      - `grade_smooth`: Road grade as percentage
  - `resolution` (optional):
    - Type: `string`
    - Values: `'low'` (~100 points), `'medium'` (~1000 points), `'high'` (~10000 points)
    - Description: Data resolution/density
  - `series_type` (optional):
    - Type: `string`
    - Values: `'time'` or `'distance'`
    - Default: `'distance'`
    - Description: Base series type for data point indexing
  - `page` (optional):
    - Type: `number`
    - Default: 1
    - Description: Page number for paginated results
  - `points_per_page` (optional):
    - Type: `number`
    - Default: 100
    - Special value: `-1` returns ALL data points split into multiple messages
    - Description: Number of data points per page

- **Output Format:**
  1. Metadata:
     - Available stream types
     - Total data points
     - Resolution and series type
     - Pagination info (current page, total pages)
  2. Statistics (where applicable):
     - Heart rate: max, min, average
     - Power: max, average, normalized power
     - Speed: max and average in km/h
  3. Stream Data:
     - Formatted time-series data for each requested stream
     - Human-readable formats (e.g., formatted time, km/h for speed)
     - Consistent numeric precision
     - Labeled data points

- **Example Request:**
  ```json
  {
    "id": 12345678,
    "types": ["time", "heartrate", "watts", "velocity_smooth", "cadence"],
    "resolution": "high",
    "points_per_page": 100,
    "page": 1
  }
  ```

- **Special Features:**
  - Smart pagination for large datasets
  - Complete data retrieval mode (points_per_page = -1)
  - Rich statistics and metadata
  - Formatted output for both human and LLM consumption
  - Automatic unit conversions

- **Notes:**
  - Requires activity:read scope
  - Not all streams are available for all activities
  - Older activities might have limited data
  - Large activities are automatically paginated
  - Stream availability depends on recording device and activity type

- **Errors:**
  - Missing/invalid token
  - Invalid activity ID
  - Insufficient permissions
  - Unavailable stream types
  - Invalid pagination parameters

---

### `get-activity-laps`

Retrieves the laps recorded for a specific Strava activity.

- **When to use:**
  - Analyze performance variations across different segments (laps) of an activity.
  - Compare lap times, speeds, heart rates, or power outputs.
  - Understand how an activity was structured (e.g., interval training).

- **Parameters:**
  - `id` (required):
    - Type: `number | string`
    - Description: The unique identifier of the Strava activity.

- **Output Format:**
  A text summary detailing each lap, including:
  - Lap Index
  - Lap Name (if available)
  - Elapsed Time (formatted as HH:MM:SS)
  - Moving Time (formatted as HH:MM:SS)
  - Distance (in km)
  - Average Speed (in km/h)
  - Max Speed (in km/h)
  - Total Elevation Gain (in meters)
  - Average Heart Rate (if available, in bpm)
  - Max Heart Rate (if available, in bpm)
  - Average Cadence (if available, in rpm)
  - Average Watts (if available, in W)

- **Example Request:**
  ```json
  {
    "id": 1234567890
  }
  ```

- **Example Response Snippet:**
  ```text
  Activity Laps Summary (ID: 1234567890):

  Lap 1: Warmup Lap
    Time: 15:02 (Moving: 14:35)
    Distance: 5.01 km
    Avg Speed: 20.82 km/h
    Max Speed: 35.50 km/h
    Elevation Gain: 50.2 m
    Avg HR: 135.5 bpm
    Max HR: 150 bpm
    Avg Cadence: 85.0 rpm

  Lap 2: Interval 1
    Time: 05:15 (Moving: 05:10)
    Distance: 2.50 km
    Avg Speed: 29.03 km/h
    Max Speed: 42.10 km/h
    Elevation Gain: 10.1 m
    Avg HR: 168.2 bpm
    Max HR: 175 bpm
    Avg Cadence: 92.1 rpm
    Avg Power: 280.5 W (Sensor)

  ...
  ```

- **Notes:**
  - Requires `activity:read` scope for public/followers activities, `activity:read_all` for private activities.
  - Lap data availability depends on the recording device and activity type (e.g., manual activities may not have laps).

- **Errors:**
  - Missing/invalid token
  - Invalid activity ID
  - Insufficient permissions
  - Activity not found

---

### `get-athlete-zones`

Retrieves the authenticated athlete's configured heart rate and power zones.

- **When to use:** When the user asks about their heart rate zones, power zones, or training zone settings.
- **Parameters:** None
- **Output Format:**
  Returns two text blocks:
  1.  A **formatted summary** detailing configured zones:
      - Heart Rate Zones: Custom status, Zone ranges, Time Distribution (if available)
      - Power Zones: Zone ranges, Time Distribution (if available)
  2.  The **complete raw JSON data** as returned by the Strava API.
- **Example Response Snippet (Summary):**
  ```text
  **Athlete Zones:**

  ❤️ **Heart Rate Zones**
     Custom Zones: No
     Zone 1: 0 - 115 bpm
     Zone 2: 115 - 145 bpm
     Zone 3: 145 - 165 bpm
     Zone 4: 165 - 180 bpm
     Zone 5: 180+ bpm

  ⚡ **Power Zones**
     Zone 1: 0 - 150 W
     Zone 2: 151 - 210 W
     Zone 3: 211 - 250 W
     Zone 4: 251 - 300 W
     Zone 5: 301 - 350 W
     Zone 6: 351 - 420 W
     Zone 7: 421+ W
     Time Distribution:
       - 0-50: 0:24:58
       - 50-100: 0:01:02
       ...
       - 450-∞: 0:05:43
  ```
- **Notes:**
  - Requires `profile:read_all` scope.
  - Zones might not be configured for all athletes.
- **Errors:**
  - Missing/invalid token
  - Insufficient permissions (Missing `profile:read_all` scope - 403 error)
  - Subscription Required (Potentially, if Strava changes API access)

---

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the LICENSE file for details. (Assuming MIT, update if different)
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "strava-mcp-server",
  "version": "1.0.1",
  "description": "MCP server for Strava API",
  "mcpName": "io.github.r-huijts/strava-mcp",
  "repository": {
    "type": "git", 
    "url": "https://github.com/r-huijts/strava-mcp"
  },
  "main": "dist/server.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js",
    "dev": "tsx src/server.ts",
    "lint": "eslint . --ext .ts",
    "setup-auth": "tsx scripts/setup-auth.ts"
  },
  "keywords": [
    "mcp",
    "strava",
    "llm",
    "ai"
  ],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.8.0",
    "axios": "^1.6.0",
    "dotenv": "^16.3.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "@types/node": "^20.11.0",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "eslint": "^8.56.0",
    "ts-node": "^10.9.0",
    "tsx": "^4.7.0",
    "typescript": "^5.3.0"
  }
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2022", // Target modern Node.js versions
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitAny": true,
    "strictNullChecks": true,

    /* If NOT transpiling with TypeScript: */
    "moduleResolution": "Bundler", // Use "NodeNext" or "Bundler" for modern Node.js
    "module": "ESNext",           // Align with "type": "module" in package.json

    /* If your code runs in the DOM: */
    // "lib": ["es2022", "dom", "dom.iterable"],

    /* If you want tsc to emit files: */
    "outDir": "dist",
    "sourceMap": true,

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
  },
  "include": ["src/**/*.ts"], // Include all TypeScript files in the src directory
  "exclude": ["node_modules", "dist"] // Exclude node_modules and the output directory
} 
```

--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
  "name": "io.github.r-huijts/strava-mcp",
  "description": "MCP server for accessing Strava API",
  "status": "active",
  "repository": {
    "url": "https://github.com/r-huijts/strava-mcp",
    "source": "github"
  },
  "version": "1.0.0",
  "packages": [
    {
      "registry_type": "npm",
      "registry_base_url": "https://registry.npmjs.org",
      "identifier": "strava-mcp-server",
      "version": "1.0.0",
      "transport": {
        "type": "stdio"
      },
      "environment_variables": [
        {
          "name": "STRAVA_CLIENT_ID",
          "description": "Your Strava API client ID",
          "is_required": true,
          "format": "string"
        },
        {
          "name": "STRAVA_CLIENT_SECRET",
          "description": "Your Strava API client secret",
          "is_required": true,
          "format": "string",
          "is_secret": true
        },
        {
          "name": "STRAVA_ACCESS_TOKEN",
          "description": "Your Strava API access token",
          "is_required": true,
          "format": "string",
          "is_secret": true
        }
      ]
    }
  ]
}

```

--------------------------------------------------------------------------------
/src/formatters.ts:
--------------------------------------------------------------------------------

```typescript
import { StravaRoute } from './stravaClient';

/**
 * Converts meters to kilometers, rounding to 2 decimal places.
 * @param meters - Distance in meters.
 * @returns Distance in kilometers as a string (e.g., "10.25 km").
 */
function metersToKmString(meters: number): string {
    if (meters === undefined || meters === null) return 'N/A';
    return (meters / 1000).toFixed(2) + ' km';
}

/**
 * Formats elevation gain in meters.
 * @param meters - Elevation gain in meters.
 * @returns Elevation gain as a string (e.g., "150 m").
 */
function formatElevation(meters: number | null | undefined): string {
    if (meters === undefined || meters === null) return 'N/A';
    return Math.round(meters) + ' m';
}

/**
 * Formats a Strava route object into a concise summary string using metric units.
 *
 * @param route - The StravaRoute object.
 * @returns A formatted string summarizing the route.
 */
export function formatRouteSummary(route: StravaRoute): string {
    const distanceKm = metersToKmString(route.distance);
    const elevation = formatElevation(route.elevation_gain);
    const date = new Date(route.created_at).toLocaleDateString();
    const type = route.type === 1 ? 'Ride' : route.type === 2 ? 'Run' : 'Walk'; // Assuming 3 is Walk based on typical Strava usage

    let summary = `📍 Route: ${route.name} (#${route.id})\n`;
    summary += `   - Type: ${type}, Distance: ${distanceKm}, Elevation: ${elevation}\n`;
    summary += `   - Created: ${date}, Segments: ${route.segments?.length ?? 'N/A'}\n`;
    if (route.description) {
        summary += `   - Description: ${route.description.substring(0, 100)}${route.description.length > 100 ? '...' : ''}\n`;
    }
    return summary;
}

// Add other shared formatters here as needed (e.g., formatActivity, formatSegment) 
```

--------------------------------------------------------------------------------
/src/tools/listAthleteClubs.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { listAthleteClubs as fetchClubs } from "../stravaClient.js"; // Renamed import

// Export the tool definition directly
export const listAthleteClubs = {
    name: "list-athlete-clubs",
    description: "Lists the clubs the authenticated athlete is a member of.",
    inputSchema: undefined,
    execute: async () => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
            console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true,
            };
        }

        try {
            console.error("Fetching athlete clubs...");
            const clubs = await fetchClubs(token);
            console.error(`Successfully fetched ${clubs?.length ?? 0} clubs.`);

            if (!clubs || clubs.length === 0) {
                return { content: [{ type: "text" as const, text: " MNo clubs found for the athlete." }] };
            }

            const clubText = clubs.map(club =>
                `
👥 **${club.name}** (ID: ${club.id})
   - Sport: ${club.sport_type}
   - Members: ${club.member_count}
   - Location: ${club.city}, ${club.state}, ${club.country}
   - Private: ${club.private ? 'Yes' : 'No'}
   - URL: ${club.url || 'N/A'}
        `.trim()
            ).join("\n---\n");

            const responseText = `**Your Strava Clubs:**\n\n${clubText}`;

            return { content: [{ type: "text" as const, text: responseText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
            console.error("Error in list-athlete-clubs tool:", errorMessage);
            return {
                content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
                isError: true,
            };
        }
    }
};

// Remove the old registration function
/*
export function registerListAthleteClubsTool(server: McpServer) {
    server.tool(
        listAthleteClubs.name,
        listAthleteClubs.description,
        listAthleteClubs.execute // No input schema
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getRoute.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { getRouteById /*, handleApiError */ } from "../stravaClient.js"; // Removed handleApiError import
import { formatRouteSummary } from "../formatters.js"; // Import shared formatter

// Zod schema for input validation
const GetRouteInputSchema = z.object({
    routeId: z.string()
        .regex(/^\d+$/, "Route ID must contain only digits")
        .refine(val => val.length > 0, "Route ID cannot be empty")
        .describe("The unique identifier of the route to fetch.")});

type GetRouteInput = z.infer<typeof GetRouteInputSchema>;

// Tool definition
export const getRouteTool = {
    name: "get-route",
    description: "Fetches detailed information about a specific route using its ID.",
    inputSchema: GetRouteInputSchema,
    execute: async (input: GetRouteInput) => {
        const { routeId } = input;
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching route details for ID: ${routeId}...`);
            const route = await getRouteById(token, routeId);
            const summary = formatRouteSummary(route); // Call shared formatter without units

            console.error(`Successfully fetched route ${routeId}.`);
            return { content: [{ type: "text" as const, text: summary }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching route ${routeId}: ${errorMessage}`);
            const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404")
                ? `Route with ID ${routeId} not found.`
                : `An unexpected error occurred while fetching route ${routeId}. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed local formatRouteSummary function

// Removed old registration function
/*
export function registerGetRouteTool(server: McpServer) {
    server.tool(
        getRoute.name,
        getRoute.description,
        getRoute.inputSchema.shape,
        getRoute.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/starSegment.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import { starSegment as updateStarStatus } from "../stravaClient.js"; // Renamed import

const StarSegmentInputSchema = z.object({
    segmentId: z.number().int().positive().describe("The unique identifier of the segment to star or unstar."),
    starred: z.boolean().describe("Set to true to star the segment, false to unstar it."),
});

type StarSegmentInput = z.infer<typeof StarSegmentInputSchema>;

// Export the tool definition directly
export const starSegment = {
    name: "star-segment",
    description: "Stars or unstars a specific segment for the authenticated athlete.",
    inputSchema: StarSegmentInputSchema,
    execute: async ({ segmentId, starred }: StarSegmentInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
            console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true,
            };
        }

        try {
            const action = starred ? 'starring' : 'unstarring';
            console.error(`Attempting to ${action} segment ID: ${segmentId}...`);

            const updatedSegment = await updateStarStatus(token, segmentId, starred);

            const successMessage = `Successfully ${action} segment: "${updatedSegment.name}" (ID: ${updatedSegment.id}). Its starred status is now: ${updatedSegment.starred}.`;
            console.error(successMessage);

            return { content: [{ type: "text" as const, text: successMessage }] };

        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
            const action = starred ? 'star' : 'unstar';
            console.error(`Error attempting to ${action} segment ID ${segmentId}:`, errorMessage);
            return {
                content: [{ type: "text" as const, text: `❌ API Error: Failed to ${action} segment ${segmentId}. ${errorMessage}` }],
                isError: true,
            };
        }
    }
};

// Removed old registration function
/*
export function registerStarSegmentTool(server: McpServer) {
    server.tool(
        starSegment.name,
        starSegment.description,
        starSegment.inputSchema.shape,
        starSegment.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getAthleteProfile.ts:
--------------------------------------------------------------------------------

```typescript
import { getAuthenticatedAthlete } from "../stravaClient.js";

// Export the tool definition directly
export const getAthleteProfile = {
    name: "get-athlete-profile",
    description: "Fetches the profile information for the authenticated athlete, including their unique numeric ID needed for other tools like get-athlete-stats.",
    // No input schema needed for this tool
    inputSchema: undefined,
    execute: async () => { // No input parameters needed
      const token = process.env.STRAVA_ACCESS_TOKEN;

      if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
        console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
        return {
          content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
          isError: true,
        };
      }

      try {
        console.error("Fetching athlete profile...");
        const athlete = await getAuthenticatedAthlete(token);
        console.error(`Successfully fetched profile for ${athlete.firstname} ${athlete.lastname} (ID: ${athlete.id}).`);

        const profileParts = [
          `👤 **Profile for ${athlete.firstname} ${athlete.lastname}** (ID: ${athlete.id})`,
          `   - Username: ${athlete.username || 'N/A'}`,
          `   - Location: ${[athlete.city, athlete.state, athlete.country].filter(Boolean).join(", ") || 'N/A'}`,
          `   - Sex: ${athlete.sex || 'N/A'}`,
          `   - Weight: ${athlete.weight ? `${athlete.weight} kg` : 'N/A'}`,
          `   - Measurement Units: ${athlete.measurement_preference}`,
          `   - Strava Summit Member: ${athlete.summit ? 'Yes' : 'No'}`,
          `   - Profile Image (Medium): ${athlete.profile_medium}`,
          `   - Joined Strava: ${athlete.created_at ? new Date(athlete.created_at).toLocaleDateString() : 'N/A'}`,
          `   - Last Updated: ${athlete.updated_at ? new Date(athlete.updated_at).toLocaleDateString() : 'N/A'}`,
        ];

        // Ensure return object matches expected structure
        const response = {
           content: [{ type: "text" as const, text: profileParts.join("\n") }]
          };
        return response;

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
        console.error("Error in get-athlete-profile tool:", errorMessage);
        return {
          content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
          isError: true,
        };
      }
    }
};

// Removed old registration function
/*
export function registerGetAthleteProfileTool(server: McpServer) {
  server.tool(
    getAthleteProfile.name,
    getAthleteProfile.description,
    getAthleteProfile.execute // No input schema
  );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getRecentActivities.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { getRecentActivities as fetchActivities } from "../stravaClient.js";
// Reverted SDK type imports

const GetRecentActivitiesInputSchema = z.object({
  perPage: z.number().int().positive().optional().default(30).describe("Number of activities to retrieve (default: 30)"),
});

type GetRecentActivitiesInput = z.infer<typeof GetRecentActivitiesInputSchema>;

// Export the tool definition directly
export const getRecentActivities = {
    name: "get-recent-activities",
    description: "Fetches the most recent activities for the authenticated athlete.",
    inputSchema: GetRecentActivitiesInputSchema,
    // Ensure the return type matches the expected structure, relying on inference
    execute: async ({ perPage }: GetRecentActivitiesInput) => {
      const token = process.env.STRAVA_ACCESS_TOKEN;

      // --- DEBUGGING: Print the token being used --- 
      console.error(`[DEBUG] Using STRAVA_ACCESS_TOKEN: ${token?.substring(0, 5)}...${token?.slice(-5)}`);
      // ---------------------------------------------

      if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
        console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
        // Use literal type for content item
        return {
          content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
          isError: true,
        };
      }

      try {
        console.error(`Fetching ${perPage} recent activities...`);
        const activities = await fetchActivities(token, perPage);
        console.error(`Successfully fetched ${activities?.length ?? 0} activities.`);

        if (!activities || activities.length === 0) {
           return {
             content: [{ type: "text" as const, text: " MNo recent activities found." }]
            };
        }

        // Map to content items with literal type
        const contentItems = activities.map(activity => {
          const dateStr = activity.start_date ? new Date(activity.start_date).toLocaleDateString() : 'N/A';
          const distanceStr = activity.distance ? `${activity.distance}m` : 'N/A';
          // Ensure each item conforms to { type: "text", text: string }
          const item: { type: "text", text: string } = {
             type: "text" as const,
             text: `🏃 ${activity.name} (ID: ${activity.id ?? 'N/A'}) — ${distanceStr} on ${dateStr}`
            };
          return item;
        });

        // Return the basic McpResponse structure
        return { content: contentItems };

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
        console.error("Error in get-recent-activities tool:", errorMessage);
        return {
          content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
          isError: true,
        };
      }
    }
};
```

--------------------------------------------------------------------------------
/src/tools/listStarredSegments.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { getAuthenticatedAthlete, listStarredSegments as fetchSegments } from "../stravaClient.js"; // Renamed import

// Export the tool definition directly
export const listStarredSegments = {
    name: "list-starred-segments",
    description: "Lists the segments starred by the authenticated athlete.",
    // No input schema needed
    inputSchema: undefined,
    execute: async () => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
            console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true,
            };
        }

        try {
            console.error("Fetching starred segments...");
            // Need athlete measurement preference for formatting distance
            const athlete = await getAuthenticatedAthlete(token);
            // Use renamed import
            const segments = await fetchSegments(token);
            console.error(`Successfully fetched ${segments?.length ?? 0} starred segments.`);

            if (!segments || segments.length === 0) {
                return { content: [{ type: "text" as const, text: " MNo starred segments found." }] };
            }

            const distanceFactor = athlete.measurement_preference === 'feet' ? 0.000621371 : 0.001;
            const distanceUnit = athlete.measurement_preference === 'feet' ? 'mi' : 'km';

            // Format the segments into a text response
            const segmentText = segments.map(segment => {
                const location = [segment.city, segment.state, segment.country].filter(Boolean).join(", ") || 'N/A';
                const distance = (segment.distance * distanceFactor).toFixed(2);
                return `
⭐ **${segment.name}** (ID: ${segment.id})
   - Activity Type: ${segment.activity_type}
   - Distance: ${distance} ${distanceUnit}
   - Avg Grade: ${segment.average_grade}%
   - Location: ${location}
   - Private: ${segment.private ? 'Yes' : 'No'}
          `.trim();
            }).join("\n---\n");

            const responseText = `**Your Starred Segments:**\n\n${segmentText}`;

            return { content: [{ type: "text" as const, text: responseText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
            console.error("Error in list-starred-segments tool:", errorMessage);
            return {
                content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
                isError: true,
            };
        }
    }
};

// Remove the old registration function
/*
export function registerListStarredSegmentsTool(server: McpServer) {
    server.tool(
        listStarredSegments.name,
        listStarredSegments.description,
        listStarredSegments.execute // No input schema
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/exportRouteTcx.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import * as fs from 'node:fs';
import * as path from 'node:path';
import { exportRouteTcx as fetchTcxData } from "../stravaClient.js";

// Define the input schema for the tool
const ExportRouteTcxInputSchema = z.object({
    routeId: z.string().describe("The ID of the Strava route to export."),
});

// Infer the input type from the schema
type ExportRouteTcxInput = z.infer<typeof ExportRouteTcxInputSchema>;

// Export the tool definition directly
export const exportRouteTcx = {
    name: "export-route-tcx",
    description: "Exports a specific Strava route in TCX format and saves it to a pre-configured local directory.",
    inputSchema: ExportRouteTcxInputSchema,
    execute: async ({ routeId }: ExportRouteTcxInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;
        if (!token) {
            // Strict return structure
            return {
                content: [{ type: "text" as const, text: "❌ Error: Missing STRAVA_ACCESS_TOKEN in .env file." }],
                isError: true
            };
        }

        const exportDir = process.env.ROUTE_EXPORT_PATH;
        if (!exportDir) {
            // Strict return structure
            return {
                content: [{ type: "text" as const, text: "❌ Error: Missing ROUTE_EXPORT_PATH in .env file. Please configure the directory for saving exports." }],
                isError: true
            };
        }

        try {
             // Ensure the directory exists, create if not
            if (!fs.existsSync(exportDir)) {
                console.error(`Export directory ${exportDir} not found, creating it...`);
                fs.mkdirSync(exportDir, { recursive: true });
            } else {
                // Check if it's a directory and writable (existing logic)
                const stats = fs.statSync(exportDir);
                if (!stats.isDirectory()) {
                    // Strict return structure
                    return {
                        content: [{ type: "text" as const, text: `❌ Error: ROUTE_EXPORT_PATH (${exportDir}) is not a valid directory.` }],
                        isError: true
                    };
                }
                fs.accessSync(exportDir, fs.constants.W_OK);
            }

            const tcxData = await fetchTcxData(token, routeId);
            const filename = `route-${routeId}.tcx`;
            const fullPath = path.join(exportDir, filename);
            fs.writeFileSync(fullPath, tcxData);

            // Strict return structure
            return {
                content: [{ type: "text" as const, text: `✅ Route ${routeId} exported successfully as TCX to: ${fullPath}` }],
            };

        } catch (err: any) {
            // Handle potential errors during directory creation/check or file writing
            console.error(`Error in export-route-tcx tool for route ${routeId}:`, err);
            let userMessage = `❌ Error exporting route ${routeId} as TCX: ${err.message}`;
             if (err.code === 'EACCES') {
                 userMessage = `❌ Error: No write permission for ROUTE_EXPORT_PATH directory (${exportDir}).`;
             }
            // Strict return structure
            return {
                content: [{ type: "text" as const, text: userMessage }],
                isError: true
            };
        }
    },
}; 
```

--------------------------------------------------------------------------------
/src/tools/exportRouteGpx.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import * as fs from 'node:fs';
import * as path from 'node:path';
import { exportRouteGpx as fetchGpxData } from "../stravaClient.js";
// import { McpServerTool } from "@modelcontextprotocol/sdk/server/mcp.js"; // Type doesn't seem exported/needed
// import { McpResponse } from "@modelcontextprotocol/sdk/server/mcp.js"; // Type doesn't seem exported

// Define the input schema for the tool
const ExportRouteGpxInputSchema = z.object({
    routeId: z.string().describe("The ID of the Strava route to export."),
});

// Infer the input type from the schema
type ExportRouteGpxInput = z.infer<typeof ExportRouteGpxInputSchema>;

// Export the tool definition directly
export const exportRouteGpx = {
    name: "export-route-gpx",
    description: "Exports a specific Strava route in GPX format and saves it to a pre-configured local directory.",
    inputSchema: ExportRouteGpxInputSchema,
    execute: async ({ routeId }: ExportRouteGpxInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;
        if (!token) {
            // Strict return structure
            return {
                content: [{ type: "text" as const, text: "❌ Error: Missing STRAVA_ACCESS_TOKEN in .env file." }],
                isError: true
            };
        }

        const exportDir = process.env.ROUTE_EXPORT_PATH;
        if (!exportDir) {
            // Strict return structure
            return {
                content: [{ type: "text" as const, text: "❌ Error: Missing ROUTE_EXPORT_PATH in .env file. Please configure the directory for saving exports." }],
                isError: true
            };
        }

        try {
            // Ensure the directory exists, create if not
            if (!fs.existsSync(exportDir)) {
                console.error(`Export directory ${exportDir} not found, creating it...`);
                fs.mkdirSync(exportDir, { recursive: true });
            } else {
                // Check if it's a directory and writable (existing logic)
                const stats = fs.statSync(exportDir);
                if (!stats.isDirectory()) {
                    // Strict return structure
                    return {
                        content: [{ type: "text" as const, text: `❌ Error: ROUTE_EXPORT_PATH (${exportDir}) is not a valid directory.` }],
                        isError: true
                    };
                }
                fs.accessSync(exportDir, fs.constants.W_OK);
            }

            const gpxData = await fetchGpxData(token, routeId);
            const filename = `route-${routeId}.gpx`;
            const fullPath = path.join(exportDir, filename);
            fs.writeFileSync(fullPath, gpxData);

            // Strict return structure
            return {
                content: [{ type: "text" as const, text: `✅ Route ${routeId} exported successfully as GPX to: ${fullPath}` }],
            };

        } catch (err: any) {
            console.error(`Error in export-route-gpx tool for route ${routeId}:`, err);
            // Strict return structure
            let userMessage = `❌ Error exporting route ${routeId} as GPX: ${err.message}`;
            if (err.code === 'EACCES') {
                userMessage = `❌ Error: No write permission for ROUTE_EXPORT_PATH directory (${exportDir}).`;
            }
            return {
                content: [{ type: "text" as const, text: userMessage }],
                isError: true
            };
        }
    },
}; 
```

--------------------------------------------------------------------------------
/test-strava-api.ts:
--------------------------------------------------------------------------------

```typescript
import axios from 'axios';
import * as dotenv from 'dotenv';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

// --- Environment Variable Loading ---
// Explicitly load .env from the project root
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const envPath = path.resolve(__dirname, '.env'); // Assumes test script is in root, .env is in root
console.log(`[TEST] Attempting to load .env file from: ${envPath}`);
dotenv.config({ path: envPath });

// Get the token
const accessToken = process.env.STRAVA_ACCESS_TOKEN;

// Basic validation
if (!accessToken || accessToken === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
  console.error('❌ Error: STRAVA_ACCESS_TOKEN is not set or is a placeholder in the .env file.');
  process.exit(1);
}

console.log(`[TEST] Using token: ${accessToken.substring(0, 5)}...${accessToken.slice(-5)}`);

// Function to test the /athlete endpoint
async function testAthleteCall() {
    console.log("--- Testing /athlete Endpoint ---");
    if (!accessToken) {
        console.error("❌ STRAVA_ACCESS_TOKEN is not set in the .env file or environment.");
        return;
    }
    console.log(`Using token: ${accessToken.substring(0, 5)}...${accessToken.substring(accessToken.length - 5)}`);

    try {
        const response = await axios.get('https://www.strava.com/api/v3/athlete', {
            headers: {
                Authorization: `Bearer ${accessToken}`,
            },
        });
        console.log("✅ Request to /athlete successful:", response.status);
        console.log("Athlete Data:", JSON.stringify(response.data, null, 2));
    } catch (error: any) {
        console.error("❌ Error calling /athlete:", error.message);
        if (error.response) {
            console.error("Status:", error.response.status);
            console.error("Data:", JSON.stringify(error.response.data, null, 2));
        }
    }
     console.log("-------------------------------\n");
}

// Function to test the /athlete/activities endpoint
async function testActivitiesCall() {
    console.log("--- Testing /athlete/activities Endpoint ---");
     if (!accessToken) {
        console.error("❌ STRAVA_ACCESS_TOKEN is not set in the .env file or environment.");
        return;
    }
    console.log(`Using token: ${accessToken.substring(0, 5)}...${accessToken.substring(accessToken.length - 5)}`);
    const perPage = 5; // Fetch 5 activities for the test

    try {
        const response = await axios.get('https://www.strava.com/api/v3/athlete/activities', {
            headers: {
                Authorization: `Bearer ${accessToken}`,
            },
            params: {
                per_page: perPage
            }
        });
        console.log(`✅ Request to /athlete/activities successful:`, response.status);
        console.log(`Received ${response.data?.length ?? 0} activities.`);
        // Optionally log activity names or IDs
        if(response.data && response.data.length > 0) {
            console.log("First activity name:", response.data[0].name);
        }
    } catch (error: any) {
        console.error("❌ Error calling /athlete/activities:", error.message);
        if (error.response) {
            console.error("Status:", error.response.status);
            console.error("Data:", JSON.stringify(error.response.data, null, 2));
        }
    }
    console.log("---------------------------------------\n");
}

// Run the tests
(async () => {
    await testAthleteCall();
    await testActivitiesCall();
})(); 
```

--------------------------------------------------------------------------------
/src/tools/listAthleteRoutes.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    listAthleteRoutes as fetchAthleteRoutes,
    StravaRoute,
    // StravaRoute is needed for the formatter
} from "../stravaClient.js";
// Remove the imported formatter since we're defining our own locally
// import { formatRouteSummary } from "../formatters.js";

// Define input schema with zod
const ListAthleteRoutesInputSchema = z.object({
    page: z.number().int().positive().optional().default(1).describe("Page number for pagination"),
    perPage: z.number().int().positive().min(1).max(50).optional().default(20).describe("Number of routes per page (max 50)"),
});

// Export the type for use in the execute function
type ListAthleteRoutesInput = z.infer<typeof ListAthleteRoutesInputSchema>;

// Function to format a route for display
function formatRouteSummary(route: StravaRoute): string {
    const distance = route.distance ? `${(route.distance / 1000).toFixed(1)} km` : 'N/A';
    const elevation = route.elevation_gain ? `${route.elevation_gain.toFixed(0)} m` : 'N/A';
    
    return `🗺️ **${route.name}** (ID: ${route.id})
   - Distance: ${distance}
   - Elevation: ${elevation}
   - Created: ${new Date(route.created_at).toLocaleDateString()}
   - Type: ${route.type === 1 ? 'Ride' : route.type === 2 ? 'Run' : 'Other'}`;
}

// Tool definition
export const listAthleteRoutesTool = {
    name: "list-athlete-routes",
    description: "Lists the routes created by the authenticated athlete, with pagination.",
    inputSchema: ListAthleteRoutesInputSchema,
    execute: async ({ page = 1, perPage = 20 }: ListAthleteRoutesInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;
        
        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true
            };
        }
        
        try {
            console.error(`Fetching routes (page ${page}, per_page: ${perPage})...`);
            
            const routes = await fetchAthleteRoutes(token, page, perPage);
            
            if (!routes || routes.length === 0) {
                console.error(`No routes found for athlete.`);
                return { content: [{ type: "text" as const, text: "No routes found for the athlete." }] };
            }
            
            console.error(`Successfully fetched ${routes.length} routes.`);
            const summaries = routes.map(route => formatRouteSummary(route));
            const responseText = `**Athlete Routes (Page ${page}):**\n\n${summaries.join("\n")}`;
            
            return { content: [{ type: "text" as const, text: responseText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error listing athlete routes (page ${page}, perPage: ${perPage}): ${errorMessage}`);
            // Removed call to handleApiError and its retry logic
            // Note: 404 is less likely for a list endpoint like this
            const userFriendlyMessage = `An unexpected error occurred while listing athlete routes. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed local formatRouteSummary and formatDuration functions

// Removed old registration function
/*
export function registerListAthleteRoutesTool(server: McpServer) {
    server.tool(
        listAthleteRoutes.name,
        listAthleteRoutes.description,
        listAthleteRoutes.inputSchema.shape,
        listAthleteRoutes.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getSegment.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    getSegmentById as fetchSegmentById,
    // handleApiError, // Removed unused import
    StravaDetailedSegment // Type needed for formatter
} from "../stravaClient.js";

// Input schema
const GetSegmentInputSchema = z.object({
    segmentId: z.number().int().positive().describe("The unique identifier of the segment to fetch.")
});
type GetSegmentInput = z.infer<typeof GetSegmentInputSchema>;

// Helper Functions (Metric Only)
function formatDistance(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return (meters / 1000).toFixed(2) + ' km';
}

function formatElevation(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return Math.round(meters) + ' m';
}

// Format segment details (Metric Only)
function formatSegmentDetails(segment: StravaDetailedSegment): string {
    const distance = formatDistance(segment.distance);
    const elevationGain = formatElevation(segment.total_elevation_gain);
    const elevationHigh = formatElevation(segment.elevation_high);
    const elevationLow = formatElevation(segment.elevation_low);

    let details = `🗺️ **Segment: ${segment.name}** (ID: ${segment.id})\n`;
    details += `   - Activity Type: ${segment.activity_type}\n`;
    details += `   - Location: ${segment.city || 'N/A'}, ${segment.state || 'N/A'}, ${segment.country || 'N/A'}\n`;
    details += `   - Distance: ${distance}\n`;
    details += `   - Avg Grade: ${segment.average_grade?.toFixed(1) ?? 'N/A'}%, Max Grade: ${segment.maximum_grade?.toFixed(1) ?? 'N/A'}%\n`;
    details += `   - Elevation: Gain ${elevationGain}, High ${elevationHigh}, Low ${elevationLow}\n`;
    details += `   - Climb Category: ${segment.climb_category ?? 'N/A'}\n`;
    details += `   - Private: ${segment.private ? 'Yes' : 'No'}\n`;
    details += `   - Starred by You: ${segment.starred ? 'Yes' : 'No'}\n`; // Assumes starred comes from auth'd user context if present
    details += `   - Total Efforts: ${segment.effort_count}, Athletes: ${segment.athlete_count}\n`;
    details += `   - Star Count: ${segment.star_count}\n`;
    details += `   - Created: ${new Date(segment.created_at).toLocaleDateString()}\n`;
    return details;
}

// Tool definition
export const getSegmentTool = {
    name: "get-segment",
    description: "Fetches detailed information about a specific segment using its ID.",
    inputSchema: GetSegmentInputSchema,
    execute: async ({ segmentId }: GetSegmentInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching details for segment ID: ${segmentId}...`);
            // Removed getAuthenticatedAthlete call
            const segment = await fetchSegmentById(token, segmentId);
            const segmentDetailsText = formatSegmentDetails(segment); // Use metric formatter

            console.error(`Successfully fetched details for segment: ${segment.name}`);
            return { content: [{ type: "text" as const, text: segmentDetailsText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching segment ${segmentId}: ${errorMessage}`);
            // Removed call to handleApiError
            const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404")
                ? `Segment with ID ${segmentId} not found.`
                : `An unexpected error occurred while fetching segment details for ID ${segmentId}. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};
```

--------------------------------------------------------------------------------
/src/tools/getAthleteZones.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { getAthleteZones as fetchAthleteZones, StravaAthleteZones } from "../stravaClient.js";
import { formatDuration } from "../server.js"; // Shared helper

const name = "get-athlete-zones";
const description = "Retrieves the authenticated athlete's configured heart rate and power zones.";

// No input schema needed for this tool
const inputSchema = z.object({}); 

type GetAthleteZonesInput = z.infer<typeof inputSchema>;

// Helper to format a single zone range
function formatZoneRange(zone: { min: number; max?: number }): string {
    return zone.max ? `${zone.min} - ${zone.max}` : `${zone.min}+`;
}

// Helper to format distribution buckets
function formatDistribution(buckets: { max: number; min: number; time: number }[] | undefined): string {
    if (!buckets || buckets.length === 0) return "  Distribution data not available.";
    
    return buckets.map(bucket => 
        `  - ${bucket.min}-${bucket.max === -1 ? '∞' : bucket.max}: ${formatDuration(bucket.time)}`
    ).join('\n');
}

// Format the zones response
function formatAthleteZones(zonesData: StravaAthleteZones): string {
    let responseText = "**Athlete Zones:**\n";

    if (zonesData.heart_rate) {
        responseText += "\n❤️ **Heart Rate Zones**\n";
        responseText += `   Custom Zones: ${zonesData.heart_rate.custom_zones ? 'Yes' : 'No'}\n`;
        zonesData.heart_rate.zones.forEach((zone, index) => {
            responseText += `   Zone ${index + 1}: ${formatZoneRange(zone)} bpm\n`;
        });
        if (zonesData.heart_rate.distribution_buckets) {
             responseText += "   Time Distribution:\n" + formatDistribution(zonesData.heart_rate.distribution_buckets) + "\n";
        }
    } else {
        responseText += "\n❤️ Heart Rate Zones: Not configured\n";
    }

    if (zonesData.power) {
        responseText += "\n⚡ **Power Zones**\n";
        zonesData.power.zones.forEach((zone, index) => {
            responseText += `   Zone ${index + 1}: ${formatZoneRange(zone)} W\n`;
        });
         if (zonesData.power.distribution_buckets) {
             responseText += "   Time Distribution:\n" + formatDistribution(zonesData.power.distribution_buckets) + "\n";
        }
    } else {
        responseText += "\n⚡ Power Zones: Not configured\n";
    }

    return responseText;
}

export const getAthleteZonesTool = {
    name,
    description: description + "\n\nOutput includes both a formatted summary and the raw JSON data.",
    inputSchema,
    execute: async (_input: GetAthleteZonesInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error("Fetching athlete zones...");
            const zonesData = await fetchAthleteZones(token);
            
            // Format the summary
            const formattedText = formatAthleteZones(zonesData);
            
            // Prepare the raw data
            const rawDataText = `\n\nRaw Athlete Zone Data:\n${JSON.stringify(zonesData, null, 2)}`;
            
            console.error("Successfully fetched athlete zones.");
            // Return both summary and raw data
            return { 
                content: [
                    { type: "text" as const, text: formattedText },
                    { type: "text" as const, text: rawDataText }
                ]
            };

        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching athlete zones: ${errorMessage}`);
            
            let userFriendlyMessage;
            // Check for common errors like missing scope (403 Forbidden)
            if (errorMessage.includes("403")) {
                 userFriendlyMessage = "🔒 Access denied. This tool requires 'profile:read_all' permission. Please re-authorize with the correct scope.";
            } else if (errorMessage.startsWith("SUBSCRIPTION_REQUIRED:")) { // In case Strava changes this later
                userFriendlyMessage = `🔒 Accessing zones might require a Strava subscription. Details: ${errorMessage}`;
            } else {
                userFriendlyMessage = `An unexpected error occurred while fetching athlete zones. Details: ${errorMessage}`;
            }

            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
}; 
```

--------------------------------------------------------------------------------
/src/tools/getActivityLaps.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { getActivityLaps as getActivityLapsClient } from "../stravaClient.js";
import { formatDuration } from "../server.js"; // Import helper

const name = "get-activity-laps";

const description = `
Retrieves detailed lap data for a specific Strava activity.

Use Cases:
- Get complete lap data including timestamps, speeds, and metrics
- Access raw values for detailed analysis or visualization
- Extract specific lap metrics for comparison or tracking

Parameters:
- id (required): The unique identifier of the Strava activity.

Output Format:
Returns both a human-readable summary and complete JSON data for each lap, including:
1. A text summary with formatted metrics
2. Raw lap data containing all fields from the Strava API:
   - Unique lap ID and indices
   - Timestamps (start_date, start_date_local)
   - Distance and timing metrics
   - Speed metrics (average and max)
   - Performance metrics (heart rate, cadence, power if available)
   - Elevation data
   - Resource state information
   - Activity and athlete references

Notes:
- Requires activity:read scope for public/followers activities, activity:read_all for private activities
- Returns complete data as received from Strava API without omissions
- All numeric values are preserved in their original precision
`;

const inputSchema = z.object({
    id: z.union([z.number(), z.string()]).describe("The identifier of the activity to fetch laps for."),
});

type GetActivityLapsInput = z.infer<typeof inputSchema>;

export const getActivityLapsTool = {
    name,
    description,
    inputSchema,
    execute: async ({ id }: GetActivityLapsInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching laps for activity ID: ${id}...`);
            const laps = await getActivityLapsClient(token, id);

            if (!laps || laps.length === 0) {
                return {
                    content: [{ type: "text" as const, text: `✅ No laps found for activity ID: ${id}` }]
                };
            }

            // Generate human-readable summary
            const lapSummaries = laps.map(lap => {
                const details = [
                    `Lap ${lap.lap_index}: ${lap.name || 'Unnamed Lap'}`,
                    `  Time: ${formatDuration(lap.elapsed_time)} (Moving: ${formatDuration(lap.moving_time)})`,
                    `  Distance: ${(lap.distance / 1000).toFixed(2)} km`,
                    `  Avg Speed: ${lap.average_speed ? (lap.average_speed * 3.6).toFixed(2) + ' km/h' : 'N/A'}`,
                    `  Max Speed: ${lap.max_speed ? (lap.max_speed * 3.6).toFixed(2) + ' km/h' : 'N/A'}`,
                    lap.total_elevation_gain ? `  Elevation Gain: ${lap.total_elevation_gain.toFixed(1)} m` : null,
                    lap.average_heartrate ? `  Avg HR: ${lap.average_heartrate.toFixed(1)} bpm` : null,
                    lap.max_heartrate ? `  Max HR: ${lap.max_heartrate} bpm` : null,
                    lap.average_cadence ? `  Avg Cadence: ${lap.average_cadence.toFixed(1)} rpm` : null,
                    lap.average_watts ? `  Avg Power: ${lap.average_watts.toFixed(1)} W ${lap.device_watts ? '(Sensor)' : ''}` : null,
                ];
                return details.filter(d => d !== null).join('\n');
            });

            const summaryText = `Activity Laps Summary (ID: ${id}):\n\n${lapSummaries.join('\n\n')}`;
            
            // Add raw data section
            const rawDataText = `\n\nComplete Lap Data:\n${JSON.stringify(laps, null, 2)}`;
            
            console.error(`Successfully fetched ${laps.length} laps for activity ${id}`);
            
            return {
                content: [
                    { type: "text" as const, text: summaryText },
                    { type: "text" as const, text: rawDataText }
                ]
            };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching laps for activity ${id}: ${errorMessage}`);
            const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404")
                ? `Activity with ID ${id} not found.`
                : `An unexpected error occurred while fetching laps for activity ${id}. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
}; 
```

--------------------------------------------------------------------------------
/src/tools/exploreSegments.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    getAuthenticatedAthlete,
    exploreSegments as fetchExploreSegments, // Renamed import
    StravaExplorerResponse
} from "../stravaClient.js";

const ExploreSegmentsInputSchema = z.object({
    bounds: z.string()
        .regex(/^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$/, "Bounds must be in the format: south_west_lat,south_west_lng,north_east_lat,north_east_lng")
        .describe("The geographical area to search, specified as a comma-separated string: south_west_lat,south_west_lng,north_east_lat,north_east_lng"),
    activityType: z.enum(["running", "riding"])
        .optional()
        .describe("Filter segments by activity type (optional: 'running' or 'riding')."),
    minCat: z.number().int().min(0).max(5).optional()
        .describe("Filter by minimum climb category (optional, 0-5). Requires riding activityType."),
    maxCat: z.number().int().min(0).max(5).optional()
        .describe("Filter by maximum climb category (optional, 0-5). Requires riding activityType."),
});

type ExploreSegmentsInput = z.infer<typeof ExploreSegmentsInputSchema>;

// Export the tool definition directly
export const exploreSegments = {
    name: "explore-segments",
    description: "Searches for popular segments within a given geographical area.",
    inputSchema: ExploreSegmentsInputSchema,
    execute: async ({ bounds, activityType, minCat, maxCat }: ExploreSegmentsInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
            console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true,
            };
        }
        if ((minCat !== undefined || maxCat !== undefined) && activityType !== 'riding') {
            return {
                content: [{ type: "text" as const, text: "❌ Input Error: Climb category filters (minCat, maxCat) require activityType to be 'riding'." }],
                isError: true,
            };
        }

        try {
            console.error(`Exploring segments within bounds: ${bounds}...`);
            const athlete = await getAuthenticatedAthlete(token);
            const response: StravaExplorerResponse = await fetchExploreSegments(token, bounds, activityType, minCat, maxCat);
            console.error(`Found ${response.segments?.length ?? 0} segments.`);

            if (!response.segments || response.segments.length === 0) {
                return { content: [{ type: "text" as const, text: " MNo segments found in the specified area with the given filters." }] };
            }

            const distanceFactor = athlete.measurement_preference === 'feet' ? 0.000621371 : 0.001;
            const distanceUnit = athlete.measurement_preference === 'feet' ? 'mi' : 'km';
            const elevationFactor = athlete.measurement_preference === 'feet' ? 3.28084 : 1;
            const elevationUnit = athlete.measurement_preference === 'feet' ? 'ft' : 'm';

            const segmentItems = response.segments.map(segment => {
                const distance = (segment.distance * distanceFactor).toFixed(2);
                const elevDifference = (segment.elev_difference * elevationFactor).toFixed(0);
                const text = `
🗺️ **${segment.name}** (ID: ${segment.id})
   - Climb: Cat ${segment.climb_category_desc} (${segment.climb_category})
   - Distance: ${distance} ${distanceUnit}
   - Avg Grade: ${segment.avg_grade}%
   - Elev Difference: ${elevDifference} ${elevationUnit}
   - Starred: ${segment.starred ? 'Yes' : 'No'}
                `.trim();
                const item: { type: "text", text: string } = { type: "text" as const, text };
                return item;
            });

            const responseText = `**Found Segments:**\n\n${segmentItems.map(item => item.text).join("\n---\n")}`;

            return { content: [{ type: "text" as const, text: responseText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
            console.error("Error in explore-segments tool:", errorMessage);
            return {
                content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
                isError: true,
            };
        }
    }
};

// Remove the old registration function
/*
export function registerExploreSegmentsTool(server: McpServer) {
    server.tool(
        exploreSegments.name,
        exploreSegments.description,
        exploreSegments.inputSchema.shape,
        exploreSegments.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getSegmentEffort.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    StravaDetailedSegmentEffort,
    getSegmentEffort as fetchSegmentEffort,
} from "../stravaClient.js";
// import { formatDuration } from "../server.js"; // Removed, now local

const GetSegmentEffortInputSchema = z.object({
    effortId: z.number().int().positive().describe("The unique identifier of the segment effort to fetch.")
});

type GetSegmentEffortInput = z.infer<typeof GetSegmentEffortInputSchema>;

// Helper Functions (Metric Only)
function formatDuration(seconds: number | null | undefined): string {
    if (seconds === null || seconds === undefined || isNaN(seconds) || seconds < 0) return 'N/A';
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    const parts: string[] = [];
    if (hours > 0) parts.push(hours.toString().padStart(2, '0'));
    parts.push(minutes.toString().padStart(2, '0'));
    parts.push(secs.toString().padStart(2, '0'));
    return parts.join(':');
}

function formatDistance(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return (meters / 1000).toFixed(2) + ' km';
}

// Format segment effort details (Metric Only)
function formatSegmentEffort(effort: StravaDetailedSegmentEffort): string {
    const movingTime = formatDuration(effort.moving_time);
    const elapsedTime = formatDuration(effort.elapsed_time);
    const distance = formatDistance(effort.distance);
    // Remove speed/pace calculations as fields are not available on effort object
    // const avgSpeed = formatSpeed(effort.average_speed);
    // const maxSpeed = formatSpeed(effort.max_speed);
    // const avgPace = formatPace(effort.average_speed);

    let details = `⏱️ **Segment Effort: ${effort.name}** (ID: ${effort.id})\n`;
    details += `   - Activity ID: ${effort.activity.id}, Athlete ID: ${effort.athlete.id}\n`;
    details += `   - Segment ID: ${effort.segment.id}\n`;
    details += `   - Date: ${new Date(effort.start_date_local).toLocaleString()}\n`;
    details += `   - Moving Time: ${movingTime}, Elapsed Time: ${elapsedTime}\n`;
    if (effort.distance !== undefined) details += `   - Distance: ${distance}\n`;
    // Remove speed/pace display lines
    // if (effort.average_speed !== undefined) { ... }
    // if (effort.max_speed !== undefined) { ... }
    if (effort.average_cadence !== undefined && effort.average_cadence !== null) details += `   - Avg Cadence: ${effort.average_cadence.toFixed(1)}\n`;
    if (effort.average_watts !== undefined && effort.average_watts !== null) details += `   - Avg Watts: ${effort.average_watts.toFixed(1)}\n`;
    if (effort.average_heartrate !== undefined && effort.average_heartrate !== null) details += `   - Avg Heart Rate: ${effort.average_heartrate.toFixed(1)} bpm\n`;
    if (effort.max_heartrate !== undefined && effort.max_heartrate !== null) details += `   - Max Heart Rate: ${effort.max_heartrate.toFixed(0)} bpm\n`;
    if (effort.kom_rank !== null) details += `   - KOM Rank: ${effort.kom_rank}\n`;
    if (effort.pr_rank !== null) details += `   - PR Rank: ${effort.pr_rank}\n`;
    details += `   - Hidden: ${effort.hidden ? 'Yes' : 'No'}\n`;

    return details;
}

// Tool definition
export const getSegmentEffortTool = {
    name: "get-segment-effort",
    description: "Fetches detailed information about a specific segment effort using its ID.",
    inputSchema: GetSegmentEffortInputSchema,
    execute: async ({ effortId }: GetSegmentEffortInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching details for segment effort ID: ${effortId}...`);
            // Removed getAuthenticatedAthlete call
            const effort = await fetchSegmentEffort(token, effortId);
            const effortDetailsText = formatSegmentEffort(effort); // Use metric formatter

            console.error(`Successfully fetched details for effort: ${effort.name}`);
            return { content: [{ type: "text" as const, text: effortDetailsText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching segment effort ${effortId}: ${errorMessage}`);

            let userFriendlyMessage;
            if (errorMessage.startsWith("SUBSCRIPTION_REQUIRED:")) {
                userFriendlyMessage = `🔒 Accessing this segment effort (ID: ${effortId}) requires a Strava subscription. Please check your subscription status.`;
            } else if (errorMessage.includes("Record Not Found") || errorMessage.includes("404")) {
                userFriendlyMessage = `Segment effort with ID ${effortId} not found.`;
            } else {
                userFriendlyMessage = `An unexpected error occurred while fetching segment effort ${effortId}. Details: ${errorMessage}`;
            }

            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed old registration function
/*
export function registerGetSegmentEffortTool(server: McpServer) {
    server.tool(
        getSegmentEffort.name,
        getSegmentEffort.description,
        getSegmentEffort.inputSchema.shape,
        getSegmentEffort.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/listSegmentEfforts.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    listSegmentEfforts as fetchSegmentEfforts,
    // handleApiError, // Removed unused import
    StravaDetailedSegmentEffort // Type needed for formatter
} from "../stravaClient.js";
// We need the formatter, but can't import the full tool. Let's copy it here for now.
// TODO: Move formatters to a shared utils.ts file

// Zod schema for input validation
const ListSegmentEffortsInputSchema = z.object({
    segmentId: z.number().int().positive().describe("The ID of the segment for which to list efforts."),
    startDateLocal: z.string().datetime({ message: "Invalid start date format. Use ISO 8601." }).optional().describe("Filter efforts starting after this ISO 8601 date-time (optional)."),
    endDateLocal: z.string().datetime({ message: "Invalid end date format. Use ISO 8601." }).optional().describe("Filter efforts ending before this ISO 8601 date-time (optional)."),
    perPage: z.number().int().positive().max(200).optional().default(30).describe("Number of efforts to return per page (default: 30, max: 200).")
});

type ListSegmentEffortsInput = z.infer<typeof ListSegmentEffortsInputSchema>;

// Helper Functions (Metric Only) - Copied locally
function formatDuration(seconds: number | null | undefined): string {
    if (seconds === null || seconds === undefined || isNaN(seconds) || seconds < 0) return 'N/A';
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    const parts: string[] = [];
    if (hours > 0) parts.push(hours.toString().padStart(2, '0'));
    parts.push(minutes.toString().padStart(2, '0'));
    parts.push(secs.toString().padStart(2, '0'));
    return parts.join(':');
}

function formatDistance(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return (meters / 1000).toFixed(2) + ' km';
}

// Format segment effort summary (Metric Only)
function formatSegmentEffort(effort: StravaDetailedSegmentEffort): string {
    const movingTime = formatDuration(effort.moving_time);
    const elapsedTime = formatDuration(effort.elapsed_time);
    const distance = formatDistance(effort.distance);

    // Basic summary: Effort ID, Date, Moving Time, Distance, PR Rank
    let summary = `⏱️ Effort ID: ${effort.id} (${new Date(effort.start_date_local).toLocaleDateString()})`;
    summary += ` | Time: ${movingTime} (Moving), ${elapsedTime} (Elapsed)`;
    summary += ` | Dist: ${distance}`;
    if (effort.pr_rank !== null) summary += ` | PR Rank: ${effort.pr_rank}`;
    if (effort.kom_rank !== null) summary += ` | KOM Rank: ${effort.kom_rank}`; // Add KOM if available
    return summary;
}

// Tool definition
export const listSegmentEffortsTool = {
    name: "list-segment-efforts",
    description: "Lists the authenticated athlete's efforts on a specific segment, optionally filtering by date.",
    inputSchema: ListSegmentEffortsInputSchema,
    execute: async ({ segmentId, startDateLocal, endDateLocal, perPage }: ListSegmentEffortsInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching segment efforts for segment ID: ${segmentId}...`);
            
            // Use the new params object structure
            const efforts = await fetchSegmentEfforts(token, segmentId, {
                startDateLocal,
                endDateLocal,
                perPage
            });

            if (!efforts || efforts.length === 0) {
                console.error(`No efforts found for segment ${segmentId} with the given filters.`);
                return { content: [{ type: "text" as const, text: `No efforts found for segment ${segmentId} matching the criteria.` }] };
            }

            console.error(`Successfully fetched ${efforts.length} efforts for segment ${segmentId}.`);
            const effortSummaries = efforts.map(effort => formatSegmentEffort(effort)); // Use metric formatter
            const responseText = `**Segment ${segmentId} Efforts:**\n\n${effortSummaries.join("\n")}`;

            return { content: [{ type: "text" as const, text: responseText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error listing efforts for segment ${segmentId}: ${errorMessage}`);

            let userFriendlyMessage;
            if (errorMessage.startsWith("SUBSCRIPTION_REQUIRED:")) {
                userFriendlyMessage = `🔒 Accessing segment efforts requires a Strava subscription. Please check your subscription status.`;
            } else if (errorMessage.includes("Record Not Found") || errorMessage.includes("404")) {
                userFriendlyMessage = `Segment with ID ${segmentId} not found (when listing efforts).`;
            } else {
                userFriendlyMessage = `An unexpected error occurred while listing efforts for segment ${segmentId}. Details: ${errorMessage}`;
            }

            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed old registration function
/*
export function registerListSegmentEffortsTool(server: McpServer) {
    server.tool(
        listSegmentEfforts.name,
        listSegmentEfforts.description,
        listSegmentEfforts.inputSchema.shape,
        listSegmentEfforts.execute
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/formatWorkoutFile.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";

// Define types for workout segments
interface WorkoutSegment {
    type: string;
    duration: {
        value: number;
        unit: 'min' | 'sec';
    };
    target: string;
    cadence?: number;
    notes?: string;
}

// Helper to convert various intensity targets to Zwift power zones
function targetToZwiftPower(target: string): number {
    // Convert various formats to percentage of FTP
    const targetLower = target.toLowerCase();
    
    // Handle direct FTP percentages
    const ftpMatch = targetLower.match(/(\d+)%\s*ftp/);
    if (ftpMatch?.[1]) {
        return parseInt(ftpMatch[1]) / 100;
    }

    // Handle common zone descriptions
    const zoneMap: { [key: string]: number } = {
        'very easy': 0.5,    // 50% FTP
        'easy': 0.6,         // 60% FTP
        'zone 1': 0.6,       // 60% FTP
        'zone 2': 0.75,      // 75% FTP
        'moderate': 0.75,    // 75% FTP
        'tempo': 0.85,       // 85% FTP
        'zone 3': 0.85,      // 85% FTP
        'threshold': 1.0,    // 100% FTP
        'zone 4': 1.0,       // 100% FTP
        'hard': 1.05,        // 105% FTP
        'zone 5': 1.1,       // 110% FTP
        'very hard': 1.15,   // 115% FTP
        'max': 1.2,          // 120% FTP
    };

    // Try to match known descriptions
    for (const [desc, power] of Object.entries(zoneMap)) {
        if (targetLower.includes(desc)) {
            return power;
        }
    }

    // Default to moderate intensity if we can't determine
    return 0.75;
}

// Parse a duration string into seconds
function parseDuration(duration: string): { value: number; unit: 'min' | 'sec' } {
    const match = duration.match(/(\d+)\s*(min|sec)/i);
    if (!match?.[1] || !match?.[2]) {
        throw new Error(`Invalid duration format: ${duration}`);
    }
    
    const value = parseInt(match[1]);
    const unit = match[2].toLowerCase() as 'min' | 'sec';
    
    return { value, unit };
}

// Parse workout text into structured segments
function parseWorkoutText(text: string): WorkoutSegment[] {
    const segments: WorkoutSegment[] = [];
    const lines = text.split('\n');

    for (const line of lines) {
        if (!line.trim().startsWith('-')) continue;

        // Extract the main parts using regex
        const segmentMatch = line.match(/^-\s*([^:]+):\s*(\d+\s*(?:min|sec))\s*at\s*([^[\n]+)(?:\s*\[([^\]]+)\])?/i);
        if (!segmentMatch?.[1] || !segmentMatch?.[2] || !segmentMatch?.[3]) continue;

        const [, type, duration, target, extras] = segmentMatch;
        
        const segment: WorkoutSegment = {
            type: type.trim(),
            duration: parseDuration(duration.trim()),
            target: target.trim()
        };

        // Parse optional extras (cadence and notes)
        if (extras) {
            const cadenceMatch = extras.match(/Cadence:\s*(\d+)/i);
            if (cadenceMatch?.[1]) {
                segment.cadence = parseInt(cadenceMatch[1]);
            }

            const notesMatch = extras.match(/Notes:\s*([^\]]+)/i);
            if (notesMatch?.[1]) {
                segment.notes = notesMatch[1].trim();
            }
        }

        segments.push(segment);
    }

    return segments;
}

// Generate ZWO XML content
function generateZwoContent(segments: WorkoutSegment[]): string {
    const workoutSegments = segments.map(segment => {
        const durationSeconds = segment.duration.unit === 'min' 
            ? segment.duration.value * 60 
            : segment.duration.value;
        
        const power = targetToZwiftPower(segment.target);
        
        const cadenceAttr = segment.cadence ? ` Cadence="${segment.cadence}"` : '';
        const showsTarget = segment.target.toLowerCase().includes('ftp') ? ' ShowsPower="1"' : '';
        
        return `        <SteadyState Duration="${durationSeconds}" Power="${power}"${cadenceAttr}${showsTarget}${segment.notes ? ` textEvent="${segment.notes}"` : ''}/>`
    }).join('\n');

    return `<workout_file>
    <author>Strava MCP Server</author>
    <name>Generated Workout</name>
    <description>Workout generated based on recent activities</description>
    <sportType>bike</sportType>
    <tags></tags>
    <workout>
${workoutSegments}
    </workout>
</workout_file>`;
}

// Tool definition
export const formatWorkoutFile = {
    name: "format-workout-file",
    description: "Formats a workout plan into a structured file format (currently supports Zwift .zwo)",
    inputSchema: z.object({
        workoutText: z.string().describe("The workout plan text in the specified format"),
        format: z.enum(['zwo']).default('zwo').describe("Output format (currently only 'zwo' is supported)")
    }),
    execute: async ({ workoutText, format }: { workoutText: string; format: 'zwo' }) => {
        try {
            // Parse the workout text into structured segments
            const segments = parseWorkoutText(workoutText);
            
            if (segments.length === 0) {
                return {
                    content: [{ 
                        type: "text", 
                        text: "❌ No valid workout segments found in the input text. Please ensure the format matches the expected pattern." 
                    }],
                    isError: true
                };
            }

            // Generate the appropriate format
            if (format === 'zwo') {
                const zwoContent = generateZwoContent(segments);
                return {
                    content: [{ 
                        type: "text", 
                        text: zwoContent,
                        mimeType: "application/xml"  // Help clients understand this is XML content
                    }]
                };
            }

            // Should never reach here due to zod validation
            throw new Error(`Unsupported format: ${format}`);
            
        } catch (error) {
            return {
                content: [{ 
                    type: "text", 
                    text: `❌ Failed to format workout: ${(error as Error).message}` 
                }],
                isError: true
            };
        }
    }
}; 
```

--------------------------------------------------------------------------------
/src/tools/getActivityDetails.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    getActivityById as fetchActivityById,
    StravaDetailedActivity // Type needed for formatter
} from "../stravaClient.js";
// import { formatDuration } from "../server.js"; // Removed, now local

// Zod schema for input validation
const GetActivityDetailsInputSchema = z.object({
    activityId: z.number().int().positive().describe("The unique identifier of the activity to fetch details for.")
});

type GetActivityDetailsInput = z.infer<typeof GetActivityDetailsInputSchema>;

// Helper Functions (Metric Only)
function formatDuration(seconds: number | null | undefined): string {
    if (seconds === null || seconds === undefined || isNaN(seconds) || seconds < 0) return 'N/A';
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    const parts: string[] = [];
    if (hours > 0) parts.push(hours.toString().padStart(2, '0'));
    parts.push(minutes.toString().padStart(2, '0'));
    parts.push(secs.toString().padStart(2, '0'));
    return parts.join(':');
}

function formatDistance(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return (meters / 1000).toFixed(2) + ' km';
}

function formatElevation(meters: number | null | undefined): string {
    if (meters === null || meters === undefined) return 'N/A';
    return Math.round(meters) + ' m';
}

function formatSpeed(mps: number | null | undefined): string {
    if (mps === null || mps === undefined) return 'N/A';
    return (mps * 3.6).toFixed(1) + ' km/h'; // Convert m/s to km/h
}

function formatPace(mps: number | null | undefined): string {
    if (mps === null || mps === undefined || mps <= 0) return 'N/A';
    const minutesPerKm = 1000 / (mps * 60);
    const minutes = Math.floor(minutesPerKm);
    const seconds = Math.round((minutesPerKm - minutes) * 60);
    return `${minutes}:${seconds.toString().padStart(2, '0')} /km`;
}

// Format activity details (Metric Only)
function formatActivityDetails(activity: StravaDetailedActivity): string {
    const date = new Date(activity.start_date_local).toLocaleString();
    const movingTime = formatDuration(activity.moving_time);
    const elapsedTime = formatDuration(activity.elapsed_time);
    const distance = formatDistance(activity.distance);
    const elevation = formatElevation(activity.total_elevation_gain);
    const avgSpeed = formatSpeed(activity.average_speed);
    const maxSpeed = formatSpeed(activity.max_speed);
    const avgPace = formatPace(activity.average_speed); // Calculate pace from speed

    let details = `🏃 **${activity.name}** (ID: ${activity.id})\n`;
    details += `   - Type: ${activity.type} (${activity.sport_type})\n`;
    details += `   - Date: ${date}\n`;
    details += `   - Moving Time: ${movingTime}, Elapsed Time: ${elapsedTime}\n`;
    if (activity.distance !== undefined) details += `   - Distance: ${distance}\n`;
    if (activity.total_elevation_gain !== undefined) details += `   - Elevation Gain: ${elevation}\n`;
    if (activity.average_speed !== undefined) {
        details += `   - Average Speed: ${avgSpeed}`;
        if (activity.type === 'Run') details += ` (Pace: ${avgPace})`;
        details += '\n';
    }
    if (activity.max_speed !== undefined) details += `   - Max Speed: ${maxSpeed}\n`;
    if (activity.average_cadence !== undefined && activity.average_cadence !== null) details += `   - Avg Cadence: ${activity.average_cadence.toFixed(1)}\n`;
    if (activity.average_watts !== undefined && activity.average_watts !== null) details += `   - Avg Watts: ${activity.average_watts.toFixed(1)}\n`;
    if (activity.average_heartrate !== undefined && activity.average_heartrate !== null) details += `   - Avg Heart Rate: ${activity.average_heartrate.toFixed(1)} bpm\n`;
    if (activity.max_heartrate !== undefined && activity.max_heartrate !== null) details += `   - Max Heart Rate: ${activity.max_heartrate.toFixed(0)} bpm\n`;
    if (activity.calories !== undefined) details += `   - Calories: ${activity.calories.toFixed(0)}\n`;
    if (activity.description) details += `   - Description: ${activity.description}\n`;
    if (activity.gear) details += `   - Gear: ${activity.gear.name}\n`;

    return details;
}

// Tool definition
export const getActivityDetailsTool = {
    name: "get-activity-details",
    description: "Fetches detailed information about a specific activity using its ID.",
    inputSchema: GetActivityDetailsInputSchema,
    execute: async ({ activityId }: GetActivityDetailsInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
            console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
            return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching details for activity ID: ${activityId}...`);
            // Removed getAuthenticatedAthlete call
            const activity = await fetchActivityById(token, activityId);
            const activityDetailsText = formatActivityDetails(activity); // Use metric formatter

            console.error(`Successfully fetched details for activity: ${activity.name}`);
            return { content: [{ type: "text" as const, text: activityDetailsText }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching activity ${activityId}: ${errorMessage}`);
            // Removed call to handleApiError
            const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404")
                ? `Activity with ID ${activityId} not found.`
                : `An unexpected error occurred while fetching activity details for ID ${activityId}. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed old registration function
/*
export function registerGetActivityDetailsTool(server: McpServer) {
  server.tool(
    getActivityDetails.name,
    getActivityDetails.description,
    getActivityDetails.inputSchema.shape,
    getActivityDetails.execute
  );
}
*/ 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";

// Import all tool definitions with the correct names
import { getAthleteProfile } from './tools/getAthleteProfile.js';
import { getAthleteStatsTool } from "./tools/getAthleteStats.js";
import { getActivityDetailsTool } from "./tools/getActivityDetails.js";
import { getRecentActivities } from "./tools/getRecentActivities.js";
import { listAthleteClubs } from './tools/listAthleteClubs.js';
import { listStarredSegments } from './tools/listStarredSegments.js';
import { getSegmentTool } from "./tools/getSegment.js";
import { exploreSegments } from './tools/exploreSegments.js';
import { starSegment } from './tools/starSegment.js';
import { getSegmentEffortTool } from './tools/getSegmentEffort.js';
import { listSegmentEffortsTool } from './tools/listSegmentEfforts.js';
import { listAthleteRoutesTool } from './tools/listAthleteRoutes.js';
import { getRouteTool } from './tools/getRoute.js';
import { exportRouteGpx } from './tools/exportRouteGpx.js';
import { exportRouteTcx } from './tools/exportRouteTcx.js';
import { getActivityStreamsTool } from './tools/getActivityStreams.js';
import { getActivityLapsTool } from './tools/getActivityLaps.js';
import { getAthleteZonesTool } from './tools/getAthleteZones.js';
import { getAllActivities } from './tools/getAllActivities.js';

// Import the actual client function
// import {
//     // exportRouteGpx as exportRouteGpxClient, // Removed unused alias
//     // exportRouteTcx as exportRouteTcxClient, // Removed unused alias
//     getActivityLaps as getActivityLapsClient
// } from './stravaClient.js';

// Load .env file explicitly from project root
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
const envPath = path.join(projectRoot, '.env');
// REMOVE THIS DEBUG LOG - Interferes with MCP Stdio transport
// console.log(`[DEBUG] Attempting to load .env file from: ${envPath}`);
dotenv.config({ path: envPath });

const server = new McpServer({
  name: "Strava MCP Server",
  version: "1.0.0"
});

// Register all tools using server.tool and the correct imported objects
server.tool(
    getAthleteProfile.name,
    getAthleteProfile.description,
    {},
    getAthleteProfile.execute
);
server.tool(
    getAthleteStatsTool.name, 
    getAthleteStatsTool.description,
    getAthleteStatsTool.inputSchema?.shape ?? {},
    getAthleteStatsTool.execute
);
server.tool(
    getActivityDetailsTool.name, 
    getActivityDetailsTool.description,
    getActivityDetailsTool.inputSchema?.shape ?? {},
    getActivityDetailsTool.execute
);
server.tool(
    getRecentActivities.name,
    getRecentActivities.description,
    getRecentActivities.inputSchema?.shape ?? {},
    getRecentActivities.execute
);
server.tool(
    listAthleteClubs.name,
    listAthleteClubs.description,
    {},
    listAthleteClubs.execute
);
server.tool(
    listStarredSegments.name,
    listStarredSegments.description,
    {},
    listStarredSegments.execute
);
server.tool(
    getSegmentTool.name, 
    getSegmentTool.description,
    getSegmentTool.inputSchema?.shape ?? {},
    getSegmentTool.execute
);
server.tool(
    exploreSegments.name,
    exploreSegments.description,
    exploreSegments.inputSchema?.shape ?? {},
    exploreSegments.execute
);
server.tool(
    starSegment.name,
    starSegment.description,
    starSegment.inputSchema?.shape ?? {},
    starSegment.execute
);
server.tool(
    getSegmentEffortTool.name, 
    getSegmentEffortTool.description,
    getSegmentEffortTool.inputSchema?.shape ?? {},
    getSegmentEffortTool.execute
);
server.tool(
    listSegmentEffortsTool.name, 
    listSegmentEffortsTool.description,
    listSegmentEffortsTool.inputSchema?.shape ?? {},
    listSegmentEffortsTool.execute
);
server.tool(
    listAthleteRoutesTool.name, 
    listAthleteRoutesTool.description,
    listAthleteRoutesTool.inputSchema?.shape ?? {},
    listAthleteRoutesTool.execute
);
server.tool(
    getRouteTool.name,
    getRouteTool.description,
    getRouteTool.inputSchema?.shape ?? {},
    getRouteTool.execute
);
server.tool(
    exportRouteGpx.name,
    exportRouteGpx.description,
    exportRouteGpx.inputSchema?.shape ?? {},
    exportRouteGpx.execute
);
server.tool(
    exportRouteTcx.name,
    exportRouteTcx.description,
    exportRouteTcx.inputSchema?.shape ?? {},
    exportRouteTcx.execute
);
server.tool(
    getActivityStreamsTool.name,
    getActivityStreamsTool.description,
    getActivityStreamsTool.inputSchema?.shape ?? {},
    getActivityStreamsTool.execute
);

// --- Register get-activity-laps tool (Simplified) ---
server.tool(
    getActivityLapsTool.name, 
    getActivityLapsTool.description,
    getActivityLapsTool.inputSchema?.shape ?? {},
    getActivityLapsTool.execute
);

// --- Register get-athlete-zones tool ---
server.tool(
    getAthleteZonesTool.name, 
    getAthleteZonesTool.description,
    getAthleteZonesTool.inputSchema?.shape ?? {},
    getAthleteZonesTool.execute
);

// --- Register get-all-activities tool ---
server.tool(
    getAllActivities.name,
    getAllActivities.description,
    getAllActivities.inputSchema?.shape ?? {},
    getAllActivities.execute
);

// --- Helper Functions ---
// Moving formatDuration to utils or keeping it here if broadly used.
// For now, it's imported by getActivityLaps.ts
export function formatDuration(seconds: number): string {
    if (isNaN(seconds) || seconds < 0) {
        return 'N/A';
    }
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);

    const parts: string[] = [];
    if (hours > 0) {
        parts.push(hours.toString().padStart(2, '0'));
    }
    parts.push(minutes.toString().padStart(2, '0'));
    parts.push(secs.toString().padStart(2, '0'));

    return parts.join(':');
}

// Removed other formatters - they are now local to their respective tools.

// --- Server Startup ---
async function startServer() {
  try {
    console.error("Starting Strava MCP Server...");
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error(`Strava MCP Server connected via Stdio. Tools registered.`);
  } catch (error) {
    console.error("Failed to start server:", error);
    process.exit(1);
  }
}

startServer();
```

--------------------------------------------------------------------------------
/scripts/setup-auth.ts:
--------------------------------------------------------------------------------

```typescript
import axios from 'axios';
import * as dotenv from 'dotenv';
import * as readline from 'readline/promises';
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';

// Define required scopes for all current and planned tools
// Explicitly request profile and activity read access.
const REQUIRED_SCOPES = 'profile:read_all,activity:read_all,activity:read,profile:write';
const REDIRECT_URI = 'http://localhost'; // Must match one configured in Strava App settings

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
const envPath = path.join(projectRoot, '.env');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

async function promptUser(question: string): Promise<string> {
  const answer = await rl.question(question);
  return answer.trim();
}

async function loadEnv(): Promise<{ clientId?: string; clientSecret?: string }> {
  try {
    await fs.access(envPath); // Check if .env exists
    const envConfig = dotenv.parse(await fs.readFile(envPath));
    return {
      clientId: envConfig.STRAVA_CLIENT_ID,
      clientSecret: envConfig.STRAVA_CLIENT_SECRET,
    };
  } catch (error) {
    console.log('.env file not found or not readable. Will prompt for all values.');
    return {};
  }
}

async function updateEnvFile(tokens: { accessToken: string; refreshToken: string }): Promise<void> {
  let envContent = '';
  try {
    envContent = await fs.readFile(envPath, 'utf-8');
  } catch (error) {
    console.log('.env file not found, creating a new one.');
  }

  const lines = envContent.split('\n');
  const newLines: string[] = [];
  let accessTokenUpdated = false;
  let refreshTokenUpdated = false;

  for (const line of lines) {
    if (line.startsWith('STRAVA_ACCESS_TOKEN=')) {
      newLines.push(`STRAVA_ACCESS_TOKEN=${tokens.accessToken}`);
      accessTokenUpdated = true;
    } else if (line.startsWith('STRAVA_REFRESH_TOKEN=')) {
      newLines.push(`STRAVA_REFRESH_TOKEN=${tokens.refreshToken}`);
      refreshTokenUpdated = true;
    } else if (line.trim() !== '') {
      newLines.push(line);
    }
  }

  if (!accessTokenUpdated) {
    newLines.push(`STRAVA_ACCESS_TOKEN=${tokens.accessToken}`);
  }
  if (!refreshTokenUpdated) {
    newLines.push(`STRAVA_REFRESH_TOKEN=${tokens.refreshToken}`);
  }

  await fs.writeFile(envPath, newLines.join('\n').trim() + '\n');
  console.log('✅ Tokens successfully saved to .env file.');
}


async function main() {
  console.log('--- Strava API Token Setup ---');

  const existingEnv = await loadEnv();
  let clientId = existingEnv.clientId;
  let clientSecret = existingEnv.clientSecret;

  if (!clientId) {
    clientId = await promptUser('Enter your Strava Application Client ID: ');
    if (!clientId) {
      console.error('❌ Client ID is required.');
      process.exit(1);
    }
  } else {
    console.log(`ℹ️ Using Client ID from .env: ${clientId}`);
  }

  if (!clientSecret) {
    clientSecret = await promptUser('Enter your Strava Application Client Secret: ');
     if (!clientSecret) {
      console.error('❌ Client Secret is required.');
      process.exit(1);
    }
  } else {
    console.log(`ℹ️ Using Client Secret from .env.`);
  }


  const authUrl = `https://www.strava.com/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${REDIRECT_URI}&approval_prompt=force&scope=${REQUIRED_SCOPES}`;

  console.log('\nStep 1: Authorize Application');
  console.log('Please visit the following URL in your browser:');
  console.log(`\n${authUrl}\n`);
  console.log(`After authorizing, Strava will redirect you to ${REDIRECT_URI}.`);
  console.log('Copy the \'code\' value from the URL in your browser\'s address bar.');
  console.log('(e.g., http://localhost/?state=&code=THIS_PART&scope=...)');

  const authCode = await promptUser('\nPaste the authorization code here: ');

  if (!authCode) {
    console.error('❌ Authorization code is required.');
    process.exit(1);
  }

  console.log('\nStep 2: Exchanging code for tokens...');

  try {
    const response = await axios.post('https://www.strava.com/oauth/token', {
        client_id: clientId,
        client_secret: clientSecret,
        code: authCode,
        grant_type: 'authorization_code',
    });

    const { access_token, refresh_token, expires_at } = response.data;

    if (!access_token || !refresh_token) {
        throw new Error('Failed to retrieve tokens from Strava.');
    }

    console.log('\n✅ Successfully obtained tokens!');
    console.log(`Access Token: ${access_token}`);
    console.log(`Refresh Token: ${refresh_token}`);
    console.log(`Access Token Expires At: ${new Date(expires_at * 1000).toLocaleString()}`);


    const save = await promptUser('\nDo you want to save these tokens to your .env file? (yes/no): ');

    if (save.toLowerCase() === 'yes' || save.toLowerCase() === 'y') {
        await updateEnvFile({ accessToken: access_token, refreshToken: refresh_token });
        // Optionally save client_id and client_secret if they weren't in .env initially
        let envContent = '';
        try {
            envContent = await fs.readFile(envPath, 'utf-8');
        } catch (readError) { /* Ignore if file doesn't exist, it was created in updateEnvFile */ }

        let needsUpdate = false;
        if (!envContent.includes('STRAVA_CLIENT_ID=')) {
            envContent = `STRAVA_CLIENT_ID=${clientId}\n` + envContent;
            needsUpdate = true;
        }
        if (!envContent.includes('STRAVA_CLIENT_SECRET=')) {
            // Add secret before tokens if they exist
            const tokenLineIndex = envContent.indexOf('STRAVA_ACCESS_TOKEN=');
            if (tokenLineIndex !== -1) {
                 envContent = envContent.substring(0, tokenLineIndex) + `STRAVA_CLIENT_SECRET=${clientSecret}\n` + envContent.substring(tokenLineIndex);
            } else {
                envContent = `STRAVA_CLIENT_SECRET=${clientSecret}\n` + envContent; // Add at the beginning if tokens aren't there
            }
            needsUpdate = true;
        }
        if (needsUpdate) {
             await fs.writeFile(envPath, envContent.trim() + '\n');
             console.log('ℹ️ Client ID and Secret also saved/updated in .env.');
        }

    } else {
        console.log('\nTokens not saved. Please store them securely yourself.');
    }

  } catch (error: any) {
    console.error('\n❌ Error exchanging code for tokens:');
     if (axios.isAxiosError(error) && error.response) {
        console.error(`Status: ${error.response.status}`);
        console.error(`Data: ${JSON.stringify(error.response.data)}`);
     } else {
        console.error(error.message || error);
     }
     process.exit(1);
  } finally {
    rl.close();
  }
}

main(); 
```

--------------------------------------------------------------------------------
/src/tools/getAthleteStats.ts:
--------------------------------------------------------------------------------

```typescript
// import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; // Removed
import { z } from "zod";
import {
    // getAuthenticatedAthlete as fetchAuthenticatedAthlete, // Removed
    getAthleteStats as fetchAthleteStats,
    // handleApiError, // Removed unused import
    StravaStats // Type needed for formatter
} from "../stravaClient.js";
// formatDuration is now local or in utils, not imported from server.ts

// Input schema: Now requires athleteId
const GetAthleteStatsInputSchema = z.object({
    athleteId: z.number().int().positive().describe("The unique identifier of the athlete to fetch stats for. Obtain this ID first by calling the get-athlete-profile tool.")
});

// Define type alias for input
type GetAthleteStatsInput = z.infer<typeof GetAthleteStatsInputSchema>;

// Remove unused formatDuration function
/*
function formatDuration(seconds: number): string {
    if (isNaN(seconds) || seconds < 0) {
        return 'N/A';
    }
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);

    const parts: string[] = [];
    if (hours > 0) {
        parts.push(hours.toString().padStart(2, '0'));
    }
    parts.push(minutes.toString().padStart(2, '0'));
    parts.push(secs.toString().padStart(2, '0'));

    return parts.join(':');
}
*/

// Helper function to format numbers as strings with labels (metric)
function formatStat(value: number | null | undefined, unit: 'km' | 'm' | 'hrs'): string {
    if (value === null || value === undefined) return 'N/A';

    let formattedValue: string;
    if (unit === 'km') {
        formattedValue = (value / 1000).toFixed(2);
    } else if (unit === 'm') {
        formattedValue = Math.round(value).toString();
    } else if (unit === 'hrs') {
        formattedValue = (value / 3600).toFixed(1);
    } else {
        formattedValue = value.toString();
    }
    return `${formattedValue} ${unit}`;
}

// Format athlete stats (metric only)
function formatStats(stats: StravaStats): string {
    const format = (label: string, total: number | null | undefined, unit: 'km' | 'm' | 'hrs', count?: number | null, time?: number | null) => {
        let line = `   - ${label}: ${formatStat(total, unit)}`;
        if (count !== undefined && count !== null) line += ` (${count} activities)`;
        if (time !== undefined && time !== null) line += ` / ${formatStat(time, 'hrs')} hours`;
        return line;
    };

    let response = "📊 **Your Strava Stats:**\n";

    if (stats.biggest_ride_distance !== undefined) {
        response += "**Rides:**\n";
        response += format("Biggest Ride", stats.biggest_ride_distance, 'km') + '\n';
    }
    if (stats.recent_ride_totals) {
        response += "*Recent Rides (last 4 weeks):*\n";
        response += format("Distance", stats.recent_ride_totals.distance, 'km', stats.recent_ride_totals.count, stats.recent_ride_totals.moving_time) + '\n';
        response += format("Elevation Gain", stats.recent_ride_totals.elevation_gain, 'm') + '\n';
    }
    if (stats.ytd_ride_totals) {
        response += "*Year-to-Date Rides:*\n";
        response += format("Distance", stats.ytd_ride_totals.distance, 'km', stats.ytd_ride_totals.count, stats.ytd_ride_totals.moving_time) + '\n';
        response += format("Elevation Gain", stats.ytd_ride_totals.elevation_gain, 'm') + '\n';
    }
    if (stats.all_ride_totals) {
        response += "*All-Time Rides:*\n";
        response += format("Distance", stats.all_ride_totals.distance, 'km', stats.all_ride_totals.count, stats.all_ride_totals.moving_time) + '\n';
        response += format("Elevation Gain", stats.all_ride_totals.elevation_gain, 'm') + '\n';
    }

    // Similar blocks for Runs and Swims if needed...
    if (stats.recent_run_totals || stats.ytd_run_totals || stats.all_run_totals) {
        response += "\n**Runs:**\n";
        if (stats.recent_run_totals) {
            response += "*Recent Runs (last 4 weeks):*\n";
            response += format("Distance", stats.recent_run_totals.distance, 'km', stats.recent_run_totals.count, stats.recent_run_totals.moving_time) + '\n';
            response += format("Elevation Gain", stats.recent_run_totals.elevation_gain, 'm') + '\n';
        }
        if (stats.ytd_run_totals) {
             response += "*Year-to-Date Runs:*\n";
             response += format("Distance", stats.ytd_run_totals.distance, 'km', stats.ytd_run_totals.count, stats.ytd_run_totals.moving_time) + '\n';
             response += format("Elevation Gain", stats.ytd_run_totals.elevation_gain, 'm') + '\n';
        }
         if (stats.all_run_totals) {
            response += "*All-Time Runs:*\n";
            response += format("Distance", stats.all_run_totals.distance, 'km', stats.all_run_totals.count, stats.all_run_totals.moving_time) + '\n';
            response += format("Elevation Gain", stats.all_run_totals.elevation_gain, 'm') + '\n';
        }
    }

    // Add Swims similarly if needed

    return response;
}

// Tool definition
export const getAthleteStatsTool = {
    name: "get-athlete-stats",
    description: "Fetches the activity statistics (recent, YTD, all-time) for a specific athlete using their ID. Requires the athleteId obtained from the get-athlete-profile tool.",
    inputSchema: GetAthleteStatsInputSchema,
    execute: async ({ athleteId }: GetAthleteStatsInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;

        if (!token) {
             console.error("Missing STRAVA_ACCESS_TOKEN environment variable.");
             return {
                content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }],
                isError: true
            };
        }

        try {
            console.error(`Fetching stats for athlete ${athleteId}...`);
            const stats = await fetchAthleteStats(token, athleteId);
            const formattedStats = formatStats(stats);

            console.error(`Successfully fetched stats for athlete ${athleteId}.`);
            return { content: [{ type: "text" as const, text: formattedStats }] };
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            console.error(`Error fetching stats for athlete ${athleteId}: ${errorMessage}`);
            const userFriendlyMessage = errorMessage.includes("Record Not Found") || errorMessage.includes("404")
                ? `Athlete with ID ${athleteId} not found (when fetching stats).`
                : `An unexpected error occurred while fetching stats for athlete ${athleteId}. Details: ${errorMessage}`;
            return {
                content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }],
                isError: true
            };
        }
    }
};

// Removed old registration function
/*
export function registerGetAthleteStatsTool(server: McpServer) {
    server.tool(
        getAthleteStats.name,
        getAthleteStats.description,
        getAthleteStats.execute // No input schema
    );
}
*/ 
```

--------------------------------------------------------------------------------
/src/tools/getAllActivities.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { getAllActivities as fetchAllActivities } from "../stravaClient.js";

// Common activity types
export const ACTIVITY_TYPES = {
    // Core types
    RIDE: "Ride",
    RUN: "Run", 
    SWIM: "Swim",
    
    // Common types
    WALK: "Walk",
    HIKE: "Hike",
    VIRTUAL_RIDE: "VirtualRide",
    VIRTUAL_RUN: "VirtualRun",
    WORKOUT: "Workout",
    WEIGHT_TRAINING: "WeightTraining",
    YOGA: "Yoga",
    
    // Winter sports
    ALPINE_SKI: "AlpineSki",
    BACKCOUNTRY_SKI: "BackcountrySki",
    NORDIC_SKI: "NordicSki",
    SNOWBOARD: "Snowboard",
    ICE_SKATE: "IceSkate",
    
    // Water sports
    KAYAKING: "Kayaking",
    ROWING: "Rowing",
    STAND_UP_PADDLING: "StandUpPaddling",
    SURFING: "Surfing",
    
    // Other
    GOLF: "Golf",
    ROCK_CLIMBING: "RockClimbing",
    SOCCER: "Soccer",
    ELLIPTICAL: "Elliptical",
    STAIR_STEPPER: "StairStepper"
} as const;

// Common sport types (more granular)
export const SPORT_TYPES = {
    MOUNTAIN_BIKE_RIDE: "MountainBikeRide",
    GRAVEL_RIDE: "GravelRide",
    E_BIKE_RIDE: "EBikeRide",
    TRAIL_RUN: "TrailRun",
    VIRTUAL_RIDE: "VirtualRide",
    VIRTUAL_RUN: "VirtualRun"
} as const;

const GetAllActivitiesInputSchema = z.object({
    startDate: z.string().optional().describe("ISO date string for activities after this date (e.g., '2024-01-01')"),
    endDate: z.string().optional().describe("ISO date string for activities before this date (e.g., '2024-12-31')"),
    activityTypes: z.array(z.string()).optional().describe("Array of activity types to filter (e.g., ['Run', 'Ride'])"),
    sportTypes: z.array(z.string()).optional().describe("Array of sport types for granular filtering (e.g., ['MountainBikeRide', 'TrailRun'])"),
    maxActivities: z.number().int().positive().optional().default(500).describe("Maximum activities to return after filtering (default: 500)"),
    maxApiCalls: z.number().int().positive().optional().default(10).describe("Maximum API calls to prevent quota exhaustion (default: 10 = ~2000 activities)"),
    perPage: z.number().int().positive().min(1).max(200).optional().default(200).describe("Activities per API call (default: 200, max: 200)")
});

type GetAllActivitiesInput = z.infer<typeof GetAllActivitiesInputSchema>;

// Helper function to format activity summary
function formatActivitySummary(activity: any): string {
    const date = activity.start_date ? new Date(activity.start_date).toLocaleDateString() : 'N/A';
    const distance = activity.distance ? `${(activity.distance / 1000).toFixed(2)} km` : 'N/A';
    const duration = activity.moving_time ? formatDuration(activity.moving_time) : 'N/A';
    const type = activity.sport_type || activity.type || 'Unknown';
    
    let emoji = '🏃';
    if (type.toLowerCase().includes('ride') || type.toLowerCase().includes('bike')) emoji = '🚴';
    else if (type.toLowerCase().includes('swim')) emoji = '🏊';
    else if (type.toLowerCase().includes('ski')) emoji = '⛷️';
    else if (type.toLowerCase().includes('hike') || type.toLowerCase().includes('walk')) emoji = '🥾';
    else if (type.toLowerCase().includes('yoga')) emoji = '🧘';
    else if (type.toLowerCase().includes('weight')) emoji = '💪';
    
    return `${emoji} ${activity.name} (${type}) - ${distance} in ${duration} on ${date}`;
}

// Helper function to format duration
function formatDuration(seconds: number): string {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;
    
    if (hours > 0) {
        return `${hours}h ${minutes}m`;
    } else if (minutes > 0) {
        return `${minutes}m ${secs}s`;
    }
    return `${secs}s`;
}

// Export the tool definition
export const getAllActivities = {
    name: "get-all-activities",
    description: "Fetches complete activity history with optional filtering by date range and activity type. Supports pagination to retrieve all activities.",
    inputSchema: GetAllActivitiesInputSchema,
    execute: async (input: GetAllActivitiesInput) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;
        
        if (!token || token === 'YOUR_STRAVA_ACCESS_TOKEN_HERE') {
            console.error("Missing or placeholder STRAVA_ACCESS_TOKEN in .env");
            return {
                content: [{ type: "text" as const, text: "❌ Configuration Error: STRAVA_ACCESS_TOKEN is missing or not set in the .env file." }],
                isError: true,
            };
        }

        const {
            startDate,
            endDate,
            activityTypes,
            sportTypes,
            maxActivities = 500,
            maxApiCalls = 10,
            perPage = 200
        } = input;

        try {
            // Convert dates to epoch timestamps if provided
            const before = endDate ? Math.floor(new Date(endDate).getTime() / 1000) : undefined;
            const after = startDate ? Math.floor(new Date(startDate).getTime() / 1000) : undefined;
            
            // Validate date inputs
            if (before && isNaN(before)) {
                return {
                    content: [{ type: "text" as const, text: "❌ Invalid endDate format. Please use ISO date format (e.g., '2024-12-31')." }],
                    isError: true
                };
            }
            if (after && isNaN(after)) {
                return {
                    content: [{ type: "text" as const, text: "❌ Invalid startDate format. Please use ISO date format (e.g., '2024-01-01')." }],
                    isError: true
                };
            }

            console.error(`Fetching activities with filters:`);
            console.error(`  Date range: ${startDate || 'any'} to ${endDate || 'any'}`);
            console.error(`  Activity types: ${activityTypes?.join(', ') || 'any'}`);
            console.error(`  Sport types: ${sportTypes?.join(', ') || 'any'}`);
            console.error(`  Max activities: ${maxActivities}, Max API calls: ${maxApiCalls}`);

            const allActivities: any[] = [];
            const filteredActivities: any[] = [];
            let apiCalls = 0;
            let currentPage = 1;
            let hasMore = true;

            // Progress callback
            const onProgress = (fetched: number, page: number) => {
                console.error(`  Page ${page}: Fetched ${fetched} total activities...`);
            };

            // Fetch activities page by page
            while (hasMore && apiCalls < maxApiCalls && filteredActivities.length < maxActivities) {
                apiCalls++;
                
                // Fetch a page of activities
                const pageActivities = await fetchAllActivities(token, {
                    page: currentPage,
                    perPage,
                    before,
                    after,
                    onProgress
                });

                // Check if we got any activities
                if (pageActivities.length === 0) {
                    hasMore = false;
                    break;
                }

                // Add to all activities
                allActivities.push(...pageActivities);

                // Apply filters if specified
                let toFilter = pageActivities;
                
                // Filter by activity type
                if (activityTypes && activityTypes.length > 0) {
                    toFilter = toFilter.filter(a => 
                        activityTypes.some(type => 
                            a.type?.toLowerCase() === type.toLowerCase()
                        )
                    );
                }
                
                // Filter by sport type (more specific)
                if (sportTypes && sportTypes.length > 0) {
                    toFilter = toFilter.filter(a => 
                        sportTypes.some(type => 
                            a.sport_type?.toLowerCase() === type.toLowerCase()
                        )
                    );
                }

                // Add filtered activities
                filteredActivities.push(...toFilter);

                // Check if we should continue
                hasMore = pageActivities.length === perPage;
                currentPage++;

                // Log progress
                console.error(`  After page ${currentPage - 1}: ${allActivities.length} fetched, ${filteredActivities.length} match filters`);
            }

            // Limit results to maxActivities
            const resultsToReturn = filteredActivities.slice(0, maxActivities);

            // Prepare summary statistics
            const stats = {
                totalFetched: allActivities.length,
                totalMatching: filteredActivities.length,
                returned: resultsToReturn.length,
                apiCalls: apiCalls
            };

            console.error(`\nFetch complete:`);
            console.error(`  Total activities fetched: ${stats.totalFetched}`);
            console.error(`  Activities matching filters: ${stats.totalMatching}`);
            console.error(`  Activities returned: ${stats.returned}`);
            console.error(`  API calls made: ${stats.apiCalls}`);

            if (resultsToReturn.length === 0) {
                return {
                    content: [{ 
                        type: "text" as const, 
                        text: `No activities found matching your criteria.\n\nStatistics:\n- Fetched ${stats.totalFetched} activities\n- ${stats.totalMatching} matched filters\n- Used ${stats.apiCalls} API calls` 
                    }]
                };
            }

            // Format activities for display
            const summaries = resultsToReturn.map(activity => formatActivitySummary(activity));
            
            // Build response text
            let responseText = `**Found ${stats.returned} activities**\n\n`;
            responseText += `📊 Statistics:\n`;
            responseText += `- Total fetched: ${stats.totalFetched}\n`;
            responseText += `- Matching filters: ${stats.totalMatching}\n`;
            responseText += `- API calls: ${stats.apiCalls}\n\n`;
            
            if (stats.returned < stats.totalMatching) {
                responseText += `⚠️ Showing first ${stats.returned} of ${stats.totalMatching} matching activities (limited by maxActivities)\n\n`;
            }
            
            responseText += `**Activities:**\n${summaries.join('\n')}`;

            return { 
                content: [{ type: "text" as const, text: responseText }] 
            };

        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
            console.error("Error in get-all-activities tool:", errorMessage);
            
            // Check for rate limiting
            if (errorMessage.includes('429')) {
                return {
                    content: [{ 
                        type: "text" as const, 
                        text: `⚠️ Rate limit reached. Please wait a few minutes before trying again.\n\nStrava API limits: 100 requests per 15 minutes, 1000 per day.` 
                    }],
                    isError: true,
                };
            }
            
            return {
                content: [{ type: "text" as const, text: `❌ API Error: ${errorMessage}` }],
                isError: true,
            };
        }
    }
};
```

--------------------------------------------------------------------------------
/src/tools/getActivityStreams.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';
import { stravaApi } from '../stravaClient.js';

// Define stream types available in Strava API
const STREAM_TYPES = [
    'time', 'distance', 'latlng', 'altitude', 'velocity_smooth',
    'heartrate', 'cadence', 'watts', 'temp', 'moving', 'grade_smooth'
] as const;

// Define resolution types
const RESOLUTION_TYPES = ['low', 'medium', 'high'] as const;

// Input schema using Zod
export const inputSchema = z.object({
    id: z.number().or(z.string()).describe(
        'The Strava activity identifier to fetch streams for. This can be obtained from activity URLs or the get-activities tool.'
    ),
    types: z.array(z.enum(STREAM_TYPES))
        .default(['time', 'distance', 'heartrate', 'cadence', 'watts'])
        .describe(
            'Array of stream types to fetch. Available types:\n' +
            '- time: Time in seconds from start\n' +
            '- distance: Distance in meters from start\n' +
            '- latlng: Array of [latitude, longitude] pairs\n' +
            '- altitude: Elevation in meters\n' +
            '- velocity_smooth: Smoothed speed in meters/second\n' +
            '- heartrate: Heart rate in beats per minute\n' +
            '- cadence: Cadence in revolutions per minute\n' +
            '- watts: Power output in watts\n' +
            '- temp: Temperature in Celsius\n' +
            '- moving: Boolean indicating if moving\n' +
            '- grade_smooth: Road grade as percentage'
        ),
    resolution: z.enum(RESOLUTION_TYPES).optional()
        .describe(
            'Optional data resolution. Affects number of data points returned:\n' +
            '- low: ~100 points\n' +
            '- medium: ~1000 points\n' +
            '- high: ~10000 points\n' +
            'Default varies based on activity length.'
        ),
    series_type: z.enum(['time', 'distance']).optional()
        .default('distance')
        .describe(
            'Optional base series type for the streams:\n' +
            '- time: Data points are indexed by time (seconds from start)\n' +
            '- distance: Data points are indexed by distance (meters from start)\n' +
            'Useful for comparing different activities or analyzing specific segments.'
        ),
    page: z.number().optional().default(1)
        .describe(
            'Optional page number for paginated results. Use with points_per_page to retrieve specific data ranges.\n' +
            'Example: page=2 with points_per_page=100 gets points 101-200.'
        ),
    points_per_page: z.number().optional().default(100)
        .describe(
            'Optional number of data points per page. Special values:\n' +
            '- Positive number: Returns that many points per page\n' +
            '- -1: Returns ALL data points split into multiple messages (~1000 points each)\n' +
            'Use -1 when you need the complete activity data for analysis.'
        )
});

// Type for the input parameters
type GetActivityStreamsParams = z.infer<typeof inputSchema>;

// Stream interfaces based on Strava API types
interface BaseStream {
    type: string;
    data: any[];
    series_type: 'distance' | 'time';
    original_size: number;
    resolution: 'low' | 'medium' | 'high';
}

interface TimeStream extends BaseStream {
    type: 'time';
    data: number[]; // seconds
}

interface DistanceStream extends BaseStream {
    type: 'distance';
    data: number[]; // meters
}

interface LatLngStream extends BaseStream {
    type: 'latlng';
    data: [number, number][]; // [latitude, longitude]
}

interface AltitudeStream extends BaseStream {
    type: 'altitude';
    data: number[]; // meters
}

interface VelocityStream extends BaseStream {
    type: 'velocity_smooth';
    data: number[]; // meters per second
}

interface HeartrateStream extends BaseStream {
    type: 'heartrate';
    data: number[]; // beats per minute
}

interface CadenceStream extends BaseStream {
    type: 'cadence';
    data: number[]; // rpm
}

interface PowerStream extends BaseStream {
    type: 'watts';
    data: number[]; // watts
}

interface TempStream extends BaseStream {
    type: 'temp';
    data: number[]; // celsius
}

interface MovingStream extends BaseStream {
    type: 'moving';
    data: boolean[];
}

interface GradeStream extends BaseStream {
    type: 'grade_smooth';
    data: number[]; // percent grade
}

type StreamSet = (TimeStream | DistanceStream | LatLngStream | AltitudeStream | 
                 VelocityStream | HeartrateStream | CadenceStream | PowerStream | 
                 TempStream | MovingStream | GradeStream)[];

// Tool definition
export const getActivityStreamsTool = {
    name: 'get-activity-streams',
    description: 
        'Retrieves detailed time-series data streams from a Strava activity. Perfect for analyzing workout metrics, ' +
        'visualizing routes, or performing detailed activity analysis.\n\n' +
        
        'Key Features:\n' +
        '1. Multiple Data Types: Access various metrics like heart rate, power, speed, GPS coordinates, etc.\n' +
        '2. Flexible Resolution: Choose data density from low (~100 points) to high (~10000 points)\n' +
        '3. Smart Pagination: Get data in manageable chunks or all at once\n' +
        '4. Rich Statistics: Includes min/max/avg for numeric streams\n' +
        '5. Formatted Output: Data is processed into human and LLM-friendly formats\n\n' +
        
        'Common Use Cases:\n' +
        '- Analyzing workout intensity through heart rate zones\n' +
        '- Calculating power metrics for cycling activities\n' +
        '- Visualizing route data using GPS coordinates\n' +
        '- Analyzing pace and elevation changes\n' +
        '- Detailed segment analysis\n\n' +
        
        'Output Format:\n' +
        '1. Metadata: Activity overview, available streams, data points\n' +
        '2. Statistics: Summary stats for each stream type (max/min/avg where applicable)\n' +
        '3. Stream Data: Actual time-series data, formatted for easy use\n\n' +
        
        'Notes:\n' +
        '- Requires activity:read scope\n' +
        '- Not all streams are available for all activities\n' +
        '- Older activities might have limited data\n' +
        '- Large activities are automatically paginated to handle size limits',
    inputSchema,
    execute: async ({ id, types, resolution, series_type, page = 1, points_per_page = 100 }: GetActivityStreamsParams) => {
        const token = process.env.STRAVA_ACCESS_TOKEN;
        if (!token) {
            return {
                content: [{ type: 'text' as const, text: '❌ Missing STRAVA_ACCESS_TOKEN in .env' }],
                isError: true
            };
        }

        try {
            // Set the auth token for this request
            stravaApi.defaults.headers.common['Authorization'] = `Bearer ${token}`;
            
            // Build query parameters
            const params: Record<string, any> = {};
            if (resolution) params.resolution = resolution;
            if (series_type) params.series_type = series_type;

            // Convert query params to string
            const queryString = new URLSearchParams(params).toString();
            
            // Build the endpoint URL with types in the path
            const endpoint = `/activities/${id}/streams/${types.join(',')}${queryString ? '?' + queryString : ''}`;
            
            const response = await stravaApi.get<StreamSet>(endpoint);
            const streams = response.data;

            if (!streams || streams.length === 0) {
                return {
                    content: [{ 
                        type: 'text' as const, 
                        text: '⚠️ No streams were returned. This could mean:\n' +
                              '1. The activity was recorded without this data\n' +
                              '2. The activity is not a GPS-based activity\n' +
                              '3. The activity is too old (Strava may not keep all stream data indefinitely)'
                    }],
                    isError: true
                };
            }

            // At this point we know streams[0] exists because we checked length > 0
            const referenceStream = streams[0]!;
            const totalPoints = referenceStream.data.length;

            // Generate stream statistics first (they're always included)
            const streamStats: Record<string, any> = {};
            streams.forEach(stream => {
                const data = stream.data;
                let stats: any = {
                    total_points: data.length,
                    resolution: stream.resolution,
                    series_type: stream.series_type
                };

                // Add type-specific statistics
                switch (stream.type) {
                    case 'heartrate':
                        const hrData = data as number[];
                        stats = {
                            ...stats,
                            max: Math.max(...hrData),
                            min: Math.min(...hrData),
                            avg: Math.round(hrData.reduce((a, b) => a + b, 0) / hrData.length)
                        };
                        break;
                    case 'watts':
                        const powerData = data as number[];
                        stats = {
                            ...stats,
                            max: Math.max(...powerData),
                            avg: Math.round(powerData.reduce((a, b) => a + b, 0) / powerData.length),
                            normalized_power: calculateNormalizedPower(powerData)
                        };
                        break;
                    case 'velocity_smooth':
                        const velocityData = data as number[];
                        stats = {
                            ...stats,
                            max_kph: Math.round(Math.max(...velocityData) * 3.6 * 10) / 10,
                            avg_kph: Math.round(velocityData.reduce((a, b) => a + b, 0) / velocityData.length * 3.6 * 10) / 10
                        };
                        break;
                }
                
                streamStats[stream.type] = stats;
            });

            // Special case: return all data in multiple messages if points_per_page is -1
            if (points_per_page === -1) {
                // Calculate optimal chunk size (aim for ~500KB per message)
                const CHUNK_SIZE = 1000; // Adjust this if needed
                const numChunks = Math.ceil(totalPoints / CHUNK_SIZE);

                // Return array of messages
                return {
                    content: [
                        // First message with metadata
                        {
                            type: 'text' as const,
                            text: `📊 Activity Stream Data (${totalPoints} points)\n` +
                                  `Will be sent in ${numChunks + 1} messages:\n` +
                                  `1. Metadata and Statistics\n` +
                                  `2-${numChunks + 1}. Stream Data (${CHUNK_SIZE} points per message)\n\n` +
                                  `Message 1/${numChunks + 1}:\n` +
                                  JSON.stringify({
                                      metadata: {
                                          available_types: streams.map(s => s.type),
                                          total_points: totalPoints,
                                          total_chunks: numChunks,
                                          chunk_size: CHUNK_SIZE,
                                          resolution: referenceStream.resolution,
                                          series_type: referenceStream.series_type
                                      },
                                      statistics: streamStats
                                  }, null, 2)
                        },
                        // Data messages
                        ...Array.from({ length: numChunks }, (_, i) => {
                            const chunkStart = i * CHUNK_SIZE;
                            const chunkEnd = Math.min(chunkStart + CHUNK_SIZE, totalPoints);
                            const streamData: Record<string, any> = { streams: {} };

                            // Process each stream for this chunk
                            streams.forEach(stream => {
                                const chunkData = stream.data.slice(chunkStart, chunkEnd);
                                let processedData: any;
                                
                                switch (stream.type) {
                                    case 'latlng':
                                        const latlngData = chunkData as [number, number][];
                                        processedData = latlngData.map(([lat, lng]) => ({
                                            latitude: Number(lat.toFixed(6)),
                                            longitude: Number(lng.toFixed(6))
                                        }));
                                        break;
                                    
                                    case 'time':
                                        const timeData = chunkData as number[];
                                        processedData = timeData.map(seconds => ({
                                            seconds_from_start: seconds,
                                            formatted: new Date(seconds * 1000).toISOString().substr(11, 8)
                                        }));
                                        break;
                                    
                                    case 'distance':
                                        const distanceData = chunkData as number[];
                                        processedData = distanceData.map(meters => ({
                                            meters,
                                            kilometers: Number((meters / 1000).toFixed(2))
                                        }));
                                        break;
                                    
                                    case 'velocity_smooth':
                                        const velocityData = chunkData as number[];
                                        processedData = velocityData.map(mps => ({
                                            meters_per_second: mps,
                                            kilometers_per_hour: Number((mps * 3.6).toFixed(1))
                                        }));
                                        break;
                                    
                                    case 'heartrate':
                                    case 'cadence':
                                    case 'watts':
                                    case 'temp':
                                        const numericData = chunkData as number[];
                                        processedData = numericData.map(v => Number(v));
                                        break;
                                    
                                    case 'grade_smooth':
                                        const gradeData = chunkData as number[];
                                        processedData = gradeData.map(grade => Number(grade.toFixed(1)));
                                        break;
                                    
                                    case 'moving':
                                        processedData = chunkData as boolean[];
                                        break;
                                    
                                    default:
                                        processedData = chunkData;
                                }

                                streamData.streams[stream.type] = processedData;
                            });

                            return {
                                type: 'text' as const,
                                text: `Message ${i + 2}/${numChunks + 1} (points ${chunkStart + 1}-${chunkEnd}):\n` +
                                      JSON.stringify(streamData, null, 2)
                            };
                        })
                    ]
                };
            }

            // Regular paginated response
            const totalPages = Math.ceil(totalPoints / points_per_page);

            // Validate page number
            if (page < 1 || page > totalPages) {
                return {
                    content: [{ 
                        type: 'text' as const, 
                        text: `❌ Invalid page number. Please specify a page between 1 and ${totalPages}`
                    }],
                    isError: true
                };
            }

            // Calculate slice indices for pagination
            const startIdx = (page - 1) * points_per_page;
            const endIdx = Math.min(startIdx + points_per_page, totalPoints);

            // Process paginated stream data
            const streamData: Record<string, any> = {
                metadata: {
                    available_types: streams.map(s => s.type),
                    total_points: totalPoints,
                    current_page: page,
                    total_pages: totalPages,
                    points_per_page,
                    points_in_page: endIdx - startIdx
                },
                statistics: streamStats,
                streams: {}
            };

            // Process each stream with pagination
            streams.forEach(stream => {
                let processedData: any;
                const paginatedData = stream.data.slice(startIdx, endIdx);
                
                switch (stream.type) {
                    case 'latlng':
                        const latlngData = paginatedData as [number, number][];
                        processedData = latlngData.map(([lat, lng]) => ({
                            latitude: Number(lat.toFixed(6)),
                            longitude: Number(lng.toFixed(6))
                        }));
                        break;
                    
                    case 'time':
                        const timeData = paginatedData as number[];
                        processedData = timeData.map(seconds => ({
                            seconds_from_start: seconds,
                            formatted: new Date(seconds * 1000).toISOString().substr(11, 8)
                        }));
                        break;
                    
                    case 'distance':
                        const distanceData = paginatedData as number[];
                        processedData = distanceData.map(meters => ({
                            meters,
                            kilometers: Number((meters / 1000).toFixed(2))
                        }));
                        break;
                    
                    case 'velocity_smooth':
                        const velocityData = paginatedData as number[];
                        processedData = velocityData.map(mps => ({
                            meters_per_second: mps,
                            kilometers_per_hour: Number((mps * 3.6).toFixed(1))
                        }));
                        break;
                    
                    case 'heartrate':
                    case 'cadence':
                    case 'watts':
                    case 'temp':
                        const numericData = paginatedData as number[];
                        processedData = numericData.map(v => Number(v));
                        break;
                    
                    case 'grade_smooth':
                        const gradeData = paginatedData as number[];
                        processedData = gradeData.map(grade => Number(grade.toFixed(1)));
                        break;
                    
                    case 'moving':
                        processedData = paginatedData as boolean[];
                        break;
                    
                    default:
                        processedData = paginatedData;
                }

                streamData.streams[stream.type] = processedData;
            });

            return {
                content: [{ 
                    type: 'text' as const, 
                    text: JSON.stringify(streamData, null, 2)
                }]
            };
        } catch (error: any) {
            const statusCode = error.response?.status;
            const errorMessage = error.response?.data?.message || error.message;
            
            let userFriendlyError = `❌ Failed to fetch activity streams (${statusCode}): ${errorMessage}\n\n`;
            userFriendlyError += 'This could be because:\n';
            userFriendlyError += '1. The activity ID is invalid\n';
            userFriendlyError += '2. You don\'t have permission to view this activity\n';
            userFriendlyError += '3. The requested stream types are not available\n';
            userFriendlyError += '4. The activity is too old and the streams have been archived';
            
            return {
                content: [{
                    type: 'text' as const,
                    text: userFriendlyError
                }],
                isError: true
            };
        }
    }
};

// Helper function to calculate normalized power
function calculateNormalizedPower(powerData: number[]): number {
    if (powerData.length < 30) return 0;
    
    // 30-second moving average
    const windowSize = 30;
    const movingAvg = [];
    for (let i = windowSize - 1; i < powerData.length; i++) {
        const window = powerData.slice(i - windowSize + 1, i + 1);
        const avg = window.reduce((a, b) => a + b, 0) / windowSize;
        movingAvg.push(Math.pow(avg, 4));
    }
    
    // Calculate normalized power
    const avgPower = Math.pow(
        movingAvg.reduce((a, b) => a + b, 0) / movingAvg.length,
        0.25
    );
    
    return Math.round(avgPower);
} 
```

--------------------------------------------------------------------------------
/src/stravaClient.ts:
--------------------------------------------------------------------------------

```typescript
import axios from "axios";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";

// --- Axios Instance & Interceptor --- 
// Create an Axios instance to apply interceptors globally for this client
export const stravaApi = axios.create({
    baseURL: 'https://www.strava.com/api/v3'
});

// Add a request interceptor (can be used for logging or modifying requests)
stravaApi.interceptors.request.use(config => {
    // REMOVE DEBUG LOGS - Interfere with MCP Stdio transport
    // let authHeaderLog = 'Not Set';
    // const authHeaderValue = config.headers?.Authorization;
    // if (typeof authHeaderValue === 'string') {
    //     authHeaderLog = `${authHeaderValue.substring(0, 12)}...[REDACTED]`;
    // }
    // console.error(`[DEBUG stravaClient] Sending Request: ${config.method?.toUpperCase()} ${config.url}`);
    // console.error(`[DEBUG stravaClient] Authorization Header: ${authHeaderLog}` );
    return config;
}, error => {
    console.error('[DEBUG stravaClient] Request Error Interceptor:', error);
    return Promise.reject(error);
});
// ----------------------------------

// Define the expected structure of a Strava activity (add more fields as needed)
const StravaActivitySchema = z.object({
    id: z.number().int().optional(), // Include ID for recent activities
    name: z.string(),
    distance: z.number(),
    start_date: z.string().datetime(),
    // Add other relevant fields from the Strava API response if needed
    // e.g., moving_time: z.number(), type: z.string(), ...
});

// Define the expected response structure for the activities endpoint
const StravaActivitiesResponseSchema = z.array(StravaActivitySchema);

// Define the expected structure for the Authenticated Athlete response
const BaseAthleteSchema = z.object({
    id: z.number().int(),
    resource_state: z.number().int(),
});
const DetailedAthleteSchema = BaseAthleteSchema.extend({
    username: z.string().nullable(),
    firstname: z.string(),
    lastname: z.string(),
    city: z.string().nullable(),
    state: z.string().nullable(),
    country: z.string().nullable(),
    sex: z.enum(["M", "F"]).nullable(),
    premium: z.boolean(),
    summit: z.boolean(),
    created_at: z.string().datetime(),
    updated_at: z.string().datetime(),
    profile_medium: z.string().url(),
    profile: z.string().url(),
    weight: z.number().nullable(),
    measurement_preference: z.enum(["feet", "meters"]).optional().nullable(),
    // Add other fields as needed (e.g., follower_count, friend_count, ftp, clubs, bikes, shoes)
});

// Type alias for the inferred athlete type
export type StravaAthlete = z.infer<typeof DetailedAthleteSchema>;

// --- Stats Schemas ---
// Schema for individual activity totals (like runs, rides, swims)
const ActivityTotalSchema = z.object({
    count: z.number().int(),
    distance: z.number(), // In meters
    moving_time: z.number().int(), // In seconds
    elapsed_time: z.number().int(), // In seconds
    elevation_gain: z.number(), // In meters
    achievement_count: z.number().int().optional().nullable(), // Optional based on Strava docs examples
});

// Schema for the overall athlete stats response
const ActivityStatsSchema = z.object({
    biggest_ride_distance: z.number().optional().nullable(),
    biggest_climb_elevation_gain: z.number().optional().nullable(),
    recent_ride_totals: ActivityTotalSchema,
    recent_run_totals: ActivityTotalSchema,
    recent_swim_totals: ActivityTotalSchema,
    ytd_ride_totals: ActivityTotalSchema,
    ytd_run_totals: ActivityTotalSchema,
    ytd_swim_totals: ActivityTotalSchema,
    all_ride_totals: ActivityTotalSchema,
    all_run_totals: ActivityTotalSchema,
    all_swim_totals: ActivityTotalSchema,
});
export type StravaStats = z.infer<typeof ActivityStatsSchema>;

// --- Club Schema ---
// Based on https://developers.strava.com/docs/reference/#api-models-SummaryClub
const SummaryClubSchema = z.object({
    id: z.number().int(),
    resource_state: z.number().int(),
    name: z.string(),
    profile_medium: z.string().url(),
    cover_photo: z.string().url().nullable(),
    cover_photo_small: z.string().url().nullable(),
    sport_type: z.string(), // cycling, running, triathlon, other
    activity_types: z.array(z.string()), // More specific types
    city: z.string(),
    state: z.string(),
    country: z.string(),
    private: z.boolean(),
    member_count: z.number().int(),
    featured: z.boolean(),
    verified: z.boolean(),
    url: z.string().nullable(),
});
export type StravaClub = z.infer<typeof SummaryClubSchema>;
const StravaClubsResponseSchema = z.array(SummaryClubSchema);

// --- Gear Schema ---
const SummaryGearSchema = z.object({
    id: z.string(),
    resource_state: z.number().int(),
    primary: z.boolean(),
    name: z.string(),
    distance: z.number(), // Distance in meters for the gear
}).nullable().optional(); // Activity might not have gear or it might be null

// --- Map Schema ---
const MapSchema = z.object({
    id: z.string(),
    summary_polyline: z.string().optional().nullable(),
    resource_state: z.number().int(),
}).nullable(); // Activity might not have a map

// --- Segment Schema ---
const SummarySegmentSchema = z.object({
    id: z.number().int(),
    name: z.string(),
    activity_type: z.string(),
    distance: z.number(),
    average_grade: z.number(),
    maximum_grade: z.number(),
    elevation_high: z.number().optional().nullable(),
    elevation_low: z.number().optional().nullable(),
    start_latlng: z.array(z.number()).optional().nullable(),
    end_latlng: z.array(z.number()).optional().nullable(),
    climb_category: z.number().int().optional().nullable(),
    city: z.string().optional().nullable(),
    state: z.string().optional().nullable(),
    country: z.string().optional().nullable(),
    private: z.boolean().optional(),
    starred: z.boolean().optional(),
});

const DetailedSegmentSchema = SummarySegmentSchema.extend({
    created_at: z.string().datetime(),
    updated_at: z.string().datetime(),
    total_elevation_gain: z.number().optional().nullable(),
    map: MapSchema, // Now defined above
    effort_count: z.number().int(),
    athlete_count: z.number().int(),
    hazardous: z.boolean(),
    star_count: z.number().int(),
});

export type StravaSegment = z.infer<typeof SummarySegmentSchema>;
export type StravaDetailedSegment = z.infer<typeof DetailedSegmentSchema>;
const StravaSegmentsResponseSchema = z.array(SummarySegmentSchema);

// --- Explorer Schemas ---
// Based on https://developers.strava.com/docs/reference/#api-models-ExplorerSegment
const ExplorerSegmentSchema = z.object({
    id: z.number().int(),
    name: z.string(),
    climb_category: z.number().int(),
    climb_category_desc: z.string(), // e.g., "NC", "4", "3", "2", "1", "HC"
    avg_grade: z.number(),
    start_latlng: z.array(z.number()),
    end_latlng: z.array(z.number()),
    elev_difference: z.number(),
    distance: z.number(), // meters
    points: z.string(), // Encoded polyline
    starred: z.boolean().optional(), // Only included if authenticated
});

// Based on https://developers.strava.com/docs/reference/#api-models-ExplorerResponse
const ExplorerResponseSchema = z.object({
    segments: z.array(ExplorerSegmentSchema),
});
export type StravaExplorerSegment = z.infer<typeof ExplorerSegmentSchema>;
export type StravaExplorerResponse = z.infer<typeof ExplorerResponseSchema>;

// --- Detailed Activity Schema ---
// Based on https://developers.strava.com/docs/reference/#api-models-DetailedActivity
const DetailedActivitySchema = z.object({
    id: z.number().int(),
    resource_state: z.number().int(), // Should be 3 for detailed
    athlete: BaseAthleteSchema, // Contains athlete ID
    name: z.string(),
    distance: z.number().optional(), // Optional for stationary activities
    moving_time: z.number().int().optional(),
    elapsed_time: z.number().int(),
    total_elevation_gain: z.number().optional(),
    type: z.string(), // e.g., "Run", "Ride"
    sport_type: z.string(),
    start_date: z.string().datetime(),
    start_date_local: z.string().datetime(),
    timezone: z.string(),
    start_latlng: z.array(z.number()).nullable(),
    end_latlng: z.array(z.number()).nullable(),
    achievement_count: z.number().int().optional(),
    kudos_count: z.number().int(),
    comment_count: z.number().int(),
    athlete_count: z.number().int().optional(), // Number of athletes on the activity
    photo_count: z.number().int(),
    map: MapSchema,
    trainer: z.boolean(),
    commute: z.boolean(),
    manual: z.boolean(),
    private: z.boolean(),
    flagged: z.boolean(),
    gear_id: z.string().nullable(), // ID of the gear used
    average_speed: z.number().optional(),
    max_speed: z.number().optional(),
    average_cadence: z.number().optional().nullable(),
    average_temp: z.number().int().optional().nullable(),
    average_watts: z.number().optional().nullable(), // Rides only
    max_watts: z.number().int().optional().nullable(), // Rides only
    weighted_average_watts: z.number().int().optional().nullable(), // Rides only
    kilojoules: z.number().optional().nullable(), // Rides only
    device_watts: z.boolean().optional().nullable(), // Rides only
    has_heartrate: z.boolean(),
    average_heartrate: z.number().optional().nullable(),
    max_heartrate: z.number().optional().nullable(),
    calories: z.number().optional(),
    description: z.string().nullable(),
    // photos: // Add PhotosSummary schema if needed
    gear: SummaryGearSchema,
    device_name: z.string().optional().nullable(),
    // segment_efforts: // Add DetailedSegmentEffort schema if needed
    // splits_metric: // Add Split schema if needed
    // splits_standard: // Add Split schema if needed
    // laps: // Add Lap schema if needed
    // best_efforts: // Add DetailedSegmentEffort schema if needed
});
export type StravaDetailedActivity = z.infer<typeof DetailedActivitySchema>;

// --- Meta Schemas ---
// Based on https://developers.strava.com/docs/reference/#api-models-MetaActivity
const MetaActivitySchema = z.object({
    id: z.number().int(),
});

// BaseAthleteSchema serves as MetaAthleteSchema (id only needed for effort)

// --- Segment Effort Schema ---
// Based on https://developers.strava.com/docs/reference/#api-models-DetailedSegmentEffort
const DetailedSegmentEffortSchema = z.object({
    id: z.number().int(),
    activity: MetaActivitySchema,
    athlete: BaseAthleteSchema,
    segment: SummarySegmentSchema, // Reuse SummarySegmentSchema
    name: z.string(), // Segment name
    elapsed_time: z.number().int(), // seconds
    moving_time: z.number().int(), // seconds
    start_date: z.string().datetime(),
    start_date_local: z.string().datetime(),
    distance: z.number(), // meters
    start_index: z.number().int().optional().nullable(),
    end_index: z.number().int().optional().nullable(),
    average_cadence: z.number().optional().nullable(),
    device_watts: z.boolean().optional().nullable(),
    average_watts: z.number().optional().nullable(),
    average_heartrate: z.number().optional().nullable(),
    max_heartrate: z.number().optional().nullable(),
    kom_rank: z.number().int().optional().nullable(), // 1-10, null if not in top 10
    pr_rank: z.number().int().optional().nullable(), // 1, 2, 3, or null
    hidden: z.boolean().optional().nullable(),
});
export type StravaDetailedSegmentEffort = z.infer<typeof DetailedSegmentEffortSchema>;

// --- Route Schema ---
// Based on https://developers.strava.com/docs/reference/#api-models-Route
const RouteSchema = z.object({
    athlete: BaseAthleteSchema, // Reuse BaseAthleteSchema
    description: z.string().nullable(),
    distance: z.number(), // meters
    elevation_gain: z.number().nullable(), // meters
    id: z.number().int(),
    id_str: z.string(),
    map: MapSchema, // Reuse MapSchema
    map_urls: z.object({ // Assuming structure based on context
        retina_url: z.string().url().optional().nullable(),
        url: z.string().url().optional().nullable(),
    }).optional().nullable(),
    name: z.string(),
    private: z.boolean(),
    resource_state: z.number().int(),
    starred: z.boolean(),
    sub_type: z.number().int(), // 1 for "road", 2 for "mtb", 3 for "cx", 4 for "trail", 5 for "mixed"
    type: z.number().int(), // 1 for "ride", 2 for "run"
    created_at: z.string().datetime(),
    updated_at: z.string().datetime(),
    estimated_moving_time: z.number().int().optional().nullable(), // seconds
    segments: z.array(SummarySegmentSchema).optional().nullable(), // Array of segments within the route
    timestamp: z.number().int().optional().nullable(), // Added based on common patterns
});
export type StravaRoute = z.infer<typeof RouteSchema>;
const StravaRoutesResponseSchema = z.array(RouteSchema);

// --- Token Refresh Functionality ---
// Calculate path to .env file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');
const envPath = path.join(projectRoot, '.env');

/**
 * Updates the .env file with new access and refresh tokens
 * @param accessToken - The new access token
 * @param refreshToken - The new refresh token
 */
async function updateTokensInEnvFile(accessToken: string, refreshToken: string): Promise<void> {
    try {
        let envContent = await fs.readFile(envPath, 'utf-8');
        const lines = envContent.split('\n');
        const newLines: string[] = [];
        let accessTokenUpdated = false;
        let refreshTokenUpdated = false;

        for (const line of lines) {
            if (line.startsWith('STRAVA_ACCESS_TOKEN=')) {
                newLines.push(`STRAVA_ACCESS_TOKEN=${accessToken}`);
                accessTokenUpdated = true;
            } else if (line.startsWith('STRAVA_REFRESH_TOKEN=')) {
                newLines.push(`STRAVA_REFRESH_TOKEN=${refreshToken}`);
                refreshTokenUpdated = true;
            } else if (line.trim() !== '') {
                newLines.push(line);
            }
        }

        if (!accessTokenUpdated) {
            newLines.push(`STRAVA_ACCESS_TOKEN=${accessToken}`);
        }
        if (!refreshTokenUpdated) {
            newLines.push(`STRAVA_REFRESH_TOKEN=${refreshToken}`);
        }

        await fs.writeFile(envPath, newLines.join('\n').trim() + '\n');
        console.error('✅ Tokens successfully refreshed and updated in .env file.');
    } catch (error) {
        console.error('Failed to update tokens in .env file:', error);
        // Continue execution even if file update fails
    }
}

/**
 * Refreshes the Strava API access token using the refresh token
 * @returns The new access token
 */
async function refreshAccessToken(): Promise<string> {
    const refreshToken = process.env.STRAVA_REFRESH_TOKEN;
    const clientId = process.env.STRAVA_CLIENT_ID;
    const clientSecret = process.env.STRAVA_CLIENT_SECRET;

    if (!refreshToken || !clientId || !clientSecret) {
        throw new Error("Missing refresh credentials in .env (STRAVA_REFRESH_TOKEN, STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET)");
    }

    try {
        console.error('🔄 Refreshing Strava access token...');
        const response = await axios.post('https://www.strava.com/oauth/token', {
            client_id: clientId,
            client_secret: clientSecret,
            refresh_token: refreshToken,
            grant_type: 'refresh_token'
        });

        // Update tokens in environment variables for the current process
        const newAccessToken = response.data.access_token;
        const newRefreshToken = response.data.refresh_token;

        if (!newAccessToken || !newRefreshToken) {
            throw new Error('Refresh response missing required tokens');
        }

        process.env.STRAVA_ACCESS_TOKEN = newAccessToken;
        process.env.STRAVA_REFRESH_TOKEN = newRefreshToken;

        // Also update .env file for persistence
        await updateTokensInEnvFile(newAccessToken, newRefreshToken);

        console.error(`✅ Token refreshed. New token expires: ${new Date(response.data.expires_at * 1000).toLocaleString()}`);
        return newAccessToken;
    } catch (error) {
        console.error('Failed to refresh access token:', error);
        throw new Error(`Failed to refresh Strava access token: ${error instanceof Error ? error.message : String(error)}`);
    }
}

/**
 * Helper function to handle API errors with token refresh capability
 * @param error - The caught error
 * @param context - The context in which the error occurred
 * @param retryFn - Optional function to retry after token refresh
 * @returns Never returns normally, always throws an error or returns via retryFn
 */
export async function handleApiError<T>(error: unknown, context: string, retryFn?: () => Promise<T>): Promise<T> {
    // Check if it's an authentication error (401) that might be fixed by refreshing the token
    if (axios.isAxiosError(error) && error.response?.status === 401 && retryFn) {
        try {
            console.error(`🔑 Authentication error in ${context}. Attempting to refresh token...`);
            await refreshAccessToken();

            // Return the result of the retry function if it succeeds
            console.error(`🔄 Retrying ${context} after token refresh...`);
            return await retryFn();
        } catch (refreshError) {
            console.error(`❌ Token refresh failed: ${refreshError instanceof Error ? refreshError.message : String(refreshError)}`);
            // Fall through to normal error handling if refresh fails
        }
    }

    // Check for subscription error (402)
    if (axios.isAxiosError(error) && error.response?.status === 402) {
        console.error(`🔒 Subscription Required in ${context}. Status: 402`);
        // Throw a specific error type or use a unique message
        throw new Error(`SUBSCRIPTION_REQUIRED: Access to this feature requires a Strava subscription. Context: ${context}`);
    }

    // Standard error handling (existing code)
    if (axios.isAxiosError(error)) {
        const status = error.response?.status || 'Unknown';
        const responseData = error.response?.data;
        const message = (typeof responseData === 'object' && responseData !== null && 'message' in responseData && typeof responseData.message === 'string')
            ? responseData.message
            : error.message;
        console.error(`Strava API request failed in ${context} with status ${status}: ${message}`);
        // Include response data in error log if helpful (be careful with sensitive data)
        if (responseData) {
            console.error(`Response data (${context}):`, JSON.stringify(responseData, null, 2));
        }
        throw new Error(`Strava API Error in ${context} (${status}): ${message}`);
    } else if (error instanceof Error) {
        console.error(`An unexpected error occurred in ${context}:`, error);
        throw new Error(`An unexpected error occurred in ${context}: ${error.message}`);
    } else {
        console.error(`An unknown error object was caught in ${context}:`, error);
        throw new Error(`An unknown error occurred in ${context}: ${String(error)}`);
    }
}

/**
 * Fetches recent activities for the authenticated athlete from the Strava API.
 *
 * @param accessToken - The Strava API access token.
 * @param perPage - The number of activities to fetch per page (default: 30).
 * @returns A promise that resolves to an array of Strava activities.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getRecentActivities(accessToken: string, perPage = 30): Promise<any[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get<unknown>("athlete/activities", {
            headers: { Authorization: `Bearer ${accessToken}` },
            params: { per_page: perPage }
        });

        const validationResult = StravaActivitiesResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API response validation failed (getRecentActivities):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }

        return validationResult.data;
    } catch (error) {
        // Pass a retry function to handleApiError
        return await handleApiError<any[]>(error, 'getRecentActivities', async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getRecentActivities(newToken, perPage);
        });
    }
}

/**
 * Fetches all activities for the authenticated athlete with pagination and date filtering.
 * Automatically handles multiple pages to retrieve complete activity history.
 *
 * @param accessToken - The Strava API access token.
 * @param params - Parameters for filtering and pagination.
 * @returns A promise that resolves to an array of all matching Strava activities.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getAllActivities(
    accessToken: string, 
    params: GetAllActivitiesParams = {}
): Promise<any[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    const { 
        page = 1, 
        perPage = 200, // Max allowed by Strava
        before,
        after,
        onProgress
    } = params;

    const allActivities: any[] = [];
    let currentPage = page;
    let hasMore = true;

    try {
        while (hasMore) {
            // Build query parameters
            const queryParams: Record<string, any> = {
                page: currentPage,
                per_page: perPage
            };
            
            // Add date filters if provided
            if (before !== undefined) queryParams.before = before;
            if (after !== undefined) queryParams.after = after;

            // Fetch current page
            const response = await stravaApi.get<unknown>("athlete/activities", {
                headers: { Authorization: `Bearer ${accessToken}` },
                params: queryParams
            });

            const validationResult = StravaActivitiesResponseSchema.safeParse(response.data);

            if (!validationResult.success) {
                console.error(`Strava API response validation failed (getAllActivities page ${currentPage}):`, validationResult.error);
                throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
            }

            const activities = validationResult.data;
            
            // Add activities to collection
            allActivities.push(...activities);
            
            // Report progress if callback provided
            if (onProgress) {
                onProgress(allActivities.length, currentPage);
            }

            // Check if we should continue
            // Stop if we got fewer activities than requested (indicating last page)
            hasMore = activities.length === perPage;
            currentPage++;

            // Add a small delay to be respectful of rate limits
            if (hasMore) {
                await new Promise(resolve => setTimeout(resolve, 100));
            }
        }

        return allActivities;
    } catch (error) {
        // If it's an auth error and we're on first page, try token refresh
        if (currentPage === 1) {
            return await handleApiError<any[]>(error, 'getAllActivities', async () => {
                const newToken = process.env.STRAVA_ACCESS_TOKEN!;
                return getAllActivities(newToken, params);
            });
        }
        // For subsequent pages, just throw the error
        throw error;
    }
}

/**
 * Fetches profile information for the authenticated athlete.
 *
 * @param accessToken - The Strava API access token.
 * @returns A promise that resolves to the detailed athlete profile.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getAuthenticatedAthlete(accessToken: string): Promise<StravaAthlete> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get<unknown>("athlete", {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        // Validate the response data against the Zod schema
        const validationResult = DetailedAthleteSchema.safeParse(response.data);

        if (!validationResult.success) {
            // Log the raw response data on validation failure for debugging
            console.error("Strava API raw response data (getAuthenticatedAthlete):", JSON.stringify(response.data, null, 2));
            console.error("Strava API response validation failed (getAuthenticatedAthlete):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        // Type assertion is safe here due to successful validation
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaAthlete>(error, 'getAuthenticatedAthlete', async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getAuthenticatedAthlete(newToken);
        });
    }
}

/**
 * Fetches activity statistics for a specific athlete.
 *
 * @param accessToken - The Strava API access token.
 * @param athleteId - The ID of the athlete whose stats are being requested.
 * @returns A promise that resolves to the athlete's activity statistics.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getAthleteStats(accessToken: string, athleteId: number): Promise<StravaStats> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!athleteId) {
        throw new Error("Athlete ID is required to fetch stats.");
    }

    try {
        const response = await stravaApi.get<unknown>(`athletes/${athleteId}/stats`, {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = ActivityStatsSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API response validation failed (getAthleteStats):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaStats>(error, `getAthleteStats for ID ${athleteId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getAthleteStats(newToken, athleteId);
        });
    }
}

/**
 * Fetches detailed information for a specific activity by its ID.
 *
 * @param accessToken - The Strava API access token.
 * @param activityId - The ID of the activity to fetch.
 * @returns A promise that resolves to the detailed activity data.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getActivityById(accessToken: string, activityId: number): Promise<StravaDetailedActivity> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!activityId) {
        throw new Error("Activity ID is required to fetch details.");
    }

    try {
        const response = await stravaApi.get<unknown>(`activities/${activityId}`, {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = DetailedActivitySchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (getActivityById: ${activityId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaDetailedActivity>(error, `getActivityById for ID ${activityId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getActivityById(newToken, activityId);
        });
    }
}

/**
 * Lists the clubs the authenticated athlete belongs to.
 *
 * @param accessToken - The Strava API access token.
 * @returns A promise that resolves to an array of the athlete's clubs.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function listAthleteClubs(accessToken: string): Promise<StravaClub[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get<unknown>("athlete/clubs", {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = StravaClubsResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API validation failed (listAthleteClubs):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaClub[]>(error, 'listAthleteClubs', async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return listAthleteClubs(newToken);
        });
    }
}

/**
 * Lists the segments starred by the authenticated athlete.
 *
 * @param accessToken - The Strava API access token.
 * @returns A promise that resolves to an array of the athlete's starred segments.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function listStarredSegments(accessToken: string): Promise<StravaSegment[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        // Strava API uses page/per_page but often defaults reasonably for lists like this.
        // Add pagination parameters if needed later.
        const response = await stravaApi.get<unknown>("segments/starred", {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = StravaSegmentsResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API validation failed (listStarredSegments):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaSegment[]>(error, 'listStarredSegments', async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return listStarredSegments(newToken);
        });
    }
}

/**
 * Fetches detailed information for a specific segment by its ID.
 *
 * @param accessToken - The Strava API access token.
 * @param segmentId - The ID of the segment to fetch.
 * @returns A promise that resolves to the detailed segment data.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getSegmentById(accessToken: string, segmentId: number): Promise<StravaDetailedSegment> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!segmentId) {
        throw new Error("Segment ID is required.");
    }

    try {
        const response = await stravaApi.get<unknown>(`segments/${segmentId}`, {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = DetailedSegmentSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (getSegmentById: ${segmentId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaDetailedSegment>(error, `getSegmentById for ID ${segmentId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getSegmentById(newToken, segmentId);
        });
    }
}

/**
 * Returns the top 10 segments matching a specified query.
 *
 * @param accessToken - The Strava API access token.
 * @param bounds - String representing the latitudes and longitudes for the corners of the search map, `latitude,longitude,latitude,longitude`.
 * @param activityType - Optional filter for activity type ("running" or "riding").
 * @param minCat - Optional minimum climb category filter.
 * @param maxCat - Optional maximum climb category filter.
 * @returns A promise that resolves to the explorer response containing matching segments.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function exploreSegments(
    accessToken: string,
    bounds: string,
    activityType?: 'running' | 'riding',
    minCat?: number,
    maxCat?: number
): Promise<StravaExplorerResponse> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!bounds || !/^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$/.test(bounds)) {
        throw new Error("Valid bounds (lat,lng,lat,lng) are required for exploring segments.");
    }

    const params: Record<string, any> = {
        bounds: bounds,
    };
    if (activityType) params.activity_type = activityType;
    if (minCat !== undefined) params.min_cat = minCat;
    if (maxCat !== undefined) params.max_cat = maxCat;

    try {
        const response = await stravaApi.get<unknown>("segments/explore", {
            headers: { Authorization: `Bearer ${accessToken}` },
            params: params
        });

        const validationResult = ExplorerResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API validation failed (exploreSegments):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaExplorerResponse>(error, `exploreSegments with bounds ${bounds}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return exploreSegments(newToken, bounds, activityType);
        });
    }
}

/**
 * Stars or unstars a segment for the authenticated athlete.
 *
 * @param accessToken - The Strava API access token.
 * @param segmentId - The ID of the segment to star/unstar.
 * @param starred - Boolean indicating whether to star (true) or unstar (false) the segment.
 * @returns A promise that resolves to the detailed segment data after the update.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function starSegment(accessToken: string, segmentId: number, starred: boolean): Promise<StravaDetailedSegment> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!segmentId) {
        throw new Error("Segment ID is required to star/unstar.");
    }
    if (starred === undefined) {
        throw new Error("Starred status (true/false) is required.");
    }

    try {
        const response = await stravaApi.put<unknown>(
            `segments/${segmentId}/starred`,
            { starred: starred }, // Data payload for the PUT request
            {
                headers: {
                    Authorization: `Bearer ${accessToken}`,
                    'Content-Type': 'application/json' // Important for PUT requests with body
                }
            }
        );

        // The response is expected to be the updated DetailedSegment
        const validationResult = DetailedSegmentSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (starSegment: ${segmentId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaDetailedSegment>(error, `starSegment for ID ${segmentId} with starred=${starred}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return starSegment(newToken, segmentId, starred);
        });
    }
}

/**
 * Fetches detailed information about a specific segment effort by its ID.
 *
 * @param accessToken - The Strava API access token.
 * @param effortId - The ID of the segment effort to fetch.
 * @returns A promise that resolves to the detailed segment effort data.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getSegmentEffort(accessToken: string, effortId: number): Promise<StravaDetailedSegmentEffort> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!effortId) {
        throw new Error("Segment Effort ID is required to fetch details.");
    }

    try {
        const response = await stravaApi.get<unknown>(`segment_efforts/${effortId}`, {
            headers: { Authorization: `Bearer ${accessToken}` }
        });

        const validationResult = DetailedSegmentEffortSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (getSegmentEffort: ${effortId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaDetailedSegmentEffort>(error, `getSegmentEffort for ID ${effortId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getSegmentEffort(newToken, effortId);
        });
    }
}

/**
 * Fetches a list of segment efforts for a given segment, filtered by date range for the authenticated athlete.
 *
 * @param accessToken - The Strava API access token.
 * @param segmentId - The ID of the segment.
 * @param startDateLocal - Optional ISO 8601 start date.
 * @param endDateLocal - Optional ISO 8601 end date.
 * @param perPage - Optional number of items per page.
 * @returns A promise that resolves to an array of segment efforts.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function listSegmentEfforts(
    accessToken: string,
    segmentId: number,
    params: SegmentEffortsParams = {}
): Promise<StravaDetailedSegmentEffort[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }
    if (!segmentId) {
        throw new Error("Segment ID is required to list efforts.");
    }

    const { startDateLocal, endDateLocal, perPage } = params;

    const queryParams: Record<string, any> = {
        segment_id: segmentId,
    };
    if (startDateLocal) queryParams.start_date_local = startDateLocal;
    if (endDateLocal) queryParams.end_date_local = endDateLocal;
    if (perPage) queryParams.per_page = perPage;

    try {
        const response = await stravaApi.get<unknown>("segment_efforts", {
            headers: { Authorization: `Bearer ${accessToken}` },
            params: queryParams
        });

        // Response is an array of DetailedSegmentEffort
        const validationResult = z.array(DetailedSegmentEffortSchema).safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (listSegmentEfforts: segment ${segmentId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaDetailedSegmentEffort[]>(error, `listSegmentEfforts for segment ID ${segmentId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return listSegmentEfforts(newToken, segmentId, params);
        });
    }
}

// Add the missing interface for segment efforts parameters
export interface SegmentEffortsParams {
    startDateLocal?: string;
    endDateLocal?: string;
    perPage?: number;
}

// Interface for getAllActivities parameters
export interface GetAllActivitiesParams {
    page?: number;
    perPage?: number;
    before?: number; // epoch timestamp in seconds
    after?: number; // epoch timestamp in seconds
    onProgress?: (fetched: number, page: number) => void;
}

/**
 * Lists routes created by a specific athlete.
 *
 * @param accessToken - The Strava API access token.
 * @param athleteId - The ID of the athlete whose routes are being requested.
 * @param page - Optional page number for pagination.
 * @param perPage - Optional number of items per page.
 * @returns A promise that resolves to an array of the athlete's routes.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function listAthleteRoutes(accessToken: string, page = 1, perPage = 30): Promise<StravaRoute[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get<unknown>("athlete/routes", {
            headers: { Authorization: `Bearer ${accessToken}` },
            params: {
                page: page,
                per_page: perPage
            }
        });

        const validationResult = StravaRoutesResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error("Strava API validation failed (listAthleteRoutes):", validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }
        return validationResult.data;

    } catch (error) {
        return await handleApiError<StravaRoute[]>(error, 'listAthleteRoutes', async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return listAthleteRoutes(newToken, page, perPage);
        });
    }
}

/**
 * Fetches detailed information for a specific route by its ID.
 *
 * @param accessToken - The Strava API access token.
 * @param routeId - The ID of the route to fetch.
 * @returns A promise that resolves to the detailed route data.
 * @throws Throws an error if the API request fails or the response format is unexpected.
 */
export async function getRouteById(accessToken: string, routeId: string): Promise<StravaRoute> {
    const url = `routes/${routeId}`;
    try {
        const response = await stravaApi.get(url, {
            headers: { Authorization: `Bearer ${accessToken}` },
        });
        // Validate the response against the Zod schema
        const validatedRoute = RouteSchema.parse(response.data);
        return validatedRoute;
    } catch (error) {
        return await handleApiError<StravaRoute>(error, `fetching route ${routeId}`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getRouteById(newToken, routeId);
        });
    }
}

/**
 * Fetches the GPX data for a specific route.
 * Note: This endpoint returns raw GPX data (XML string), not JSON.
 * @param accessToken Strava API access token
 * @param routeId The ID of the route to export
 * @returns Promise resolving to the GPX data as a string
 */
export async function exportRouteGpx(accessToken: string, routeId: string): Promise<string> {
    const url = `routes/${routeId}/export_gpx`;
    try {
        // Expecting text/xml response, Axios should handle it as string
        const response = await stravaApi.get<string>(url, {
            headers: { Authorization: `Bearer ${accessToken}` },
            // Ensure response is treated as text
            responseType: 'text',
        });
        if (typeof response.data !== 'string') {
            throw new Error('Invalid response format received from Strava API for GPX export.');
        }
        return response.data;
    } catch (error) {
        return await handleApiError<string>(error, `exporting route ${routeId} as GPX`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return exportRouteGpx(newToken, routeId);
        });
    }
}

/**
 * Fetches the TCX data for a specific route.
 * Note: This endpoint returns raw TCX data (XML string), not JSON.
 * @param accessToken Strava API access token
 * @param routeId The ID of the route to export
 * @returns Promise resolving to the TCX data as a string
 */
export async function exportRouteTcx(accessToken: string, routeId: string): Promise<string> {
    const url = `routes/${routeId}/export_tcx`;
    try {
        // Expecting text/xml response, Axios should handle it as string
        const response = await stravaApi.get<string>(url, {
            headers: { Authorization: `Bearer ${accessToken}` },
            // Ensure response is treated as text
            responseType: 'text',
        });
        if (typeof response.data !== 'string') {
            throw new Error('Invalid response format received from Strava API for TCX export.');
        }
        return response.data;
    } catch (error) {
        return await handleApiError<string>(error, `exporting route ${routeId} as TCX`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return exportRouteTcx(newToken, routeId);
        });
    }
}

// --- Lap Schema ---
// Based on https://developers.strava.com/docs/reference/#api-models-Lap and user-provided image
const LapSchema = z.object({
    id: z.number().int(),
    resource_state: z.number().int(),
    name: z.string(),
    activity: BaseAthleteSchema, // Reusing BaseAthleteSchema for {id, resource_state}
    athlete: BaseAthleteSchema, // Reusing BaseAthleteSchema for {id, resource_state}
    elapsed_time: z.number().int(), // In seconds
    moving_time: z.number().int(), // In seconds
    start_date: z.string().datetime(),
    start_date_local: z.string().datetime(),
    distance: z.number(), // In meters
    start_index: z.number().int().optional().nullable(), // Index in the activity stream
    end_index: z.number().int().optional().nullable(), // Index in the activity stream
    total_elevation_gain: z.number().optional().nullable(), // In meters
    average_speed: z.number().optional().nullable(), // In meters per second
    max_speed: z.number().optional().nullable(), // In meters per second
    average_cadence: z.number().optional().nullable(), // RPM
    average_watts: z.number().optional().nullable(), // Rides only
    device_watts: z.boolean().optional().nullable(), // Whether power sensor was used
    average_heartrate: z.number().optional().nullable(), // Average heart rate during lap
    max_heartrate: z.number().optional().nullable(), // Max heart rate during lap
    lap_index: z.number().int(), // The position of this lap in the activity
    split: z.number().int().optional().nullable(), // Associated split number (e.g., for marathons)
});

export type StravaLap = z.infer<typeof LapSchema>;
const StravaLapsResponseSchema = z.array(LapSchema);

/**
 * Retrieves the laps for a specific activity.
 * @param accessToken The Strava API access token.
 * @param activityId The ID of the activity.
 * @returns A promise resolving to an array of lap objects.
 */
export async function getActivityLaps(accessToken: string, activityId: number | string): Promise<StravaLap[]> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get(`/activities/${activityId}/laps`, {
            headers: { Authorization: `Bearer ${accessToken}` },
        });

        const validationResult = StravaLapsResponseSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (getActivityLaps: ${activityId}):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }

        return validationResult.data;
    } catch (error) {
        return await handleApiError<StravaLap[]>(error, `getActivityLaps(${activityId})`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getActivityLaps(newToken, activityId);
        });
    }
}

// --- Zone Schemas ---
const DistributionBucketSchema = z.object({
    max: z.number(),
    min: z.number(),
    time: z.number().int(), // Time in seconds spent in this bucket
});

const ZoneSchema = z.object({
    min: z.number(),
    max: z.number().optional(), // Max might be absent for the last zone
});

const HeartRateZoneSchema = z.object({
    custom_zones: z.boolean(),
    zones: z.array(ZoneSchema),
    distribution_buckets: z.array(DistributionBucketSchema).optional(), // Optional based on sample
    resource_state: z.number().int().optional(), // Optional based on sample
    sensor_based: z.boolean().optional(), // Optional based on sample
    points: z.number().int().optional(), // Optional based on sample
    type: z.literal('heartrate').optional(), // Optional based on sample
});

const PowerZoneSchema = z.object({
    zones: z.array(ZoneSchema),
    distribution_buckets: z.array(DistributionBucketSchema).optional(), // Optional based on sample
    resource_state: z.number().int().optional(), // Optional based on sample
    sensor_based: z.boolean().optional(), // Optional based on sample
    points: z.number().int().optional(), // Optional based on sample
    type: z.literal('power').optional(), // Optional based on sample
});

// Combined Zones Response Schema
const AthleteZonesSchema = z.object({
    heart_rate: HeartRateZoneSchema.optional(), // Heart rate zones might not be set
    power: PowerZoneSchema.optional(), // Power zones might not be set
});

export type StravaAthleteZones = z.infer<typeof AthleteZonesSchema>;

/**
 * Retrieves the heart rate and power zones for the authenticated athlete.
 * @param accessToken The Strava API access token.
 * @returns A promise resolving to the athlete's zone data.
 */
export async function getAthleteZones(accessToken: string): Promise<StravaAthleteZones> {
    if (!accessToken) {
        throw new Error("Strava access token is required.");
    }

    try {
        const response = await stravaApi.get<unknown>("/athlete/zones", {
            headers: { Authorization: `Bearer ${accessToken}` },
        });

        const validationResult = AthleteZonesSchema.safeParse(response.data);

        if (!validationResult.success) {
            console.error(`Strava API validation failed (getAthleteZones):`, validationResult.error);
            throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`);
        }

        return validationResult.data;
    } catch (error) {
        // Note: This endpoint requires profile:read_all scope
        // Handle potential 403 Forbidden if scope is missing, or 402 if it becomes sub-only?
        return await handleApiError<StravaAthleteZones>(error, `getAthleteZones`, async () => {
            // Use new token from environment after refresh
            const newToken = process.env.STRAVA_ACCESS_TOKEN!;
            return getAthleteZones(newToken);
        });
    }
}

```