diff --git a/README.md b/README.md index fe6297d..a52fc8a 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,33 @@ docker build -t strava-mcp-server:latest . docker run --rm -p 8000:8000 --env-file .env strava-mcp-server:latest ``` -### Local Python (uv) +### Local Python (PyPI) -We use `uv` for lightning-fast dependency management and task execution. +The easiest way to get started is using our **interactive onboarding wizard**. You don't even need to clone the repository: + +```bash +# 1. Start the interactive setup wizard +uvx --from strava-mcp-server-hnrx auth + +# 2. Run the MCP server +uvx --from strava-mcp-server-hnrx server +``` + +The wizard will guide you through creating a Strava API application and automatically save your credentials to a `.env` file. + +### Installation + +If you prefer a traditional installation: + +```bash +pip install strava-mcp-server-hnrx +# or +uv add strava-mcp-server-hnrx +``` + +### Running from source + +If you want to contribute or run the latest dev version: ```bash git clone https://git.hnrx.net/hnrx/strava-mcp-server.git @@ -80,17 +104,6 @@ uv run server uv run auth ``` -### Run on the fly with `uvx` (No git clone required) - -You can run the server directly from the repository without cloning it manually by using `uvx`: - -```bash -# Set up your .env file in the current directory first! -uvx --from git+https://git.hnrx.net/hnrx/strava-mcp-server.git server -``` - -*(If you are already inside the cloned directory, you can also just run `uvx --from . server`)* - --- ## Strava API Setup diff --git a/pyproject.toml b/pyproject.toml index 5f0a4a2..13e6736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ ] [project.urls] -Homepage = "https://git.hnrx.net/hnrx/strava-mcp-server" +Homepage = "https://strava-mcp.web.s3.hnrx.net" Repository = "https://git.hnrx.net/hnrx/strava-mcp-server" "Bug Tracker" = "https://git.hnrx.net/hnrx/strava-mcp-server/issues" diff --git a/src/strava_mcp_server/get_token.py b/src/strava_mcp_server/get_token.py index 5b82478..0c42325 100644 --- a/src/strava_mcp_server/get_token.py +++ b/src/strava_mcp_server/get_token.py @@ -27,6 +27,101 @@ REDIRECT_URI = "http://localhost:8765/callback" SCOPES = "profile:read_all,activity:read_all,activity:read,profile:write" +class SetupHandler(BaseHTTPRequestHandler): + setup_done = False + client_id = "" + client_secret = "" + + def do_GET(self): + if self.path == "/setup" or self.path == "/": + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write(self._get_setup_page().encode("utf-8")) + elif self.path.startswith("/save"): + parsed = urlparse(self.path) + params = parse_qs(parsed.query) + if "id" in params and "secret" in params: + SetupHandler.client_id = params["id"][0].strip() + SetupHandler.client_secret = params["secret"][0].strip() + SetupHandler.setup_done = True + + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write( + """ + + + +
+

Settings Saved!

+

Redirecting to Strava Authorization...

+
+ + + """.encode("utf-8") + ) + else: + self.send_response(400) + self.end_headers() + self.wfile.write(b"Missing parameters") + else: + self.send_response(404) + self.end_headers() + + def _get_setup_page(self): + return """ + + + + + + Strava MCP Setup + + + +
+

Strava MCP Setup

+
+ How to get your credentials: +
    +
  1. Go to Strava API Settings.
  2. +
  3. Create an app (any name/category).
  4. +
  5. Set "Authorization Callback Domain" to localhost.
  6. +
  7. Copy your Client ID and Client Secret below.
  8. +
+
+
+ + + + + +
+
+ + + """ + + def log_message(self, format, *args): + pass + + class CallbackHandler(BaseHTTPRequestHandler): client_id: str = "" client_secret: str = "" @@ -51,65 +146,45 @@ class CallbackHandler(BaseHTTPRequestHandler): }, ) response.raise_for_status() - self.tokens = response.json() + CallbackHandler.tokens = response.json() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() - refresh_token = self.tokens.get("refresh_token") + refresh_token = CallbackHandler.tokens.get("refresh_token") self.wfile.write( f""" -
- -

Authorization successful!

-

You have successfully authenticated with Strava. You can now close this window.

-
+ +

Setup Complete!

+

Your Strava account is now connected to the MCP server.

-

1. Local Setup (.env)

-

Copy the following block into your .env file in the project root:

- Your .env content: +

UPDATED .ENV CONTENT:

STRAVA_CLIENT_ID={self.client_id}
 STRAVA_CLIENT_SECRET={self.client_secret}
 STRAVA_REFRESH_TOKEN={refresh_token}
+

This information has been automatically saved to your .env file.

- -

2. Kubernetes Setup (Secret)

-

If you are deploying this server to Kubernetes, run the following command to create the required Secret:

-
- Kubectl Command: -
kubectl create secret generic strava-mcp-server-secret \\
-  --from-literal=STRAVA_CLIENT_ID={self.client_id} \\
-  --from-literal=STRAVA_CLIENT_SECRET={self.client_secret} \\
-  --from-literal=STRAVA_REFRESH_TOKEN={refresh_token}
-
- -

- — Strava MCP Server Authorization Helper — -

+

You can now close this window and restart the server.

""".encode("utf-8") ) except Exception as e: - self.error = str(e) + CallbackHandler.error = str(e) self.send_response(500) self.end_headers() self.wfile.write(f"Error exchanging token: {e}".encode()) @@ -123,11 +198,70 @@ STRAVA_REFRESH_TOKEN={refresh_token} pass # Suppress server logs -def main(): - if not CLIENT_ID or not CLIENT_SECRET: - print("❌ Missing STRAVA_CLIENT_ID or STRAVA_CLIENT_SECRET in .env") - return +def save_to_env(client_id, client_secret, refresh_token=None): + env_path = ".env" + lines = [] + if os.path.exists(env_path): + with open(env_path, "r") as f: + lines = f.readlines() + keys_to_set = { + "STRAVA_CLIENT_ID": client_id, + "STRAVA_CLIENT_SECRET": client_secret, + } + if refresh_token: + keys_to_set["STRAVA_REFRESH_TOKEN"] = refresh_token + + new_lines = [] + seen_keys = set() + + for line in lines: + matched = False + for key, value in keys_to_set.items(): + if line.startswith(f"{key}="): + new_lines.append(f"{key}={value}\n") + seen_keys.add(key) + matched = True + break + if not matched: + new_lines.append(line) + + for key, value in keys_to_set.items(): + if key not in seen_keys: + new_lines.append(f"{key}={value}\n") + + with open(env_path, "w") as f: + f.writelines(new_lines) + + # Debug output + saved_keys = ", ".join(keys_to_set.keys()) + print(f"📝 Updated .env with: {saved_keys}") + + +def main(): + global CLIENT_ID, CLIENT_SECRET + + # 1. Start Setup Wizard if credentials missing + if not CLIENT_ID or not CLIENT_SECRET: + print( + "ℹ️ Missing credentials. Starting setup wizard at http://localhost:8765 ..." + ) + print("Please enter your Client ID and Secret in the browser window.") + webbrowser.open("http://localhost:8765/setup") + + HTTPServer.allow_reuse_address = True + setup_server = HTTPServer(("localhost", 8765), SetupHandler) + try: + while not SetupHandler.setup_done: + setup_server.handle_request() + finally: + setup_server.server_close() + + CLIENT_ID = SetupHandler.client_id + CLIENT_SECRET = SetupHandler.client_secret + save_to_env(CLIENT_ID, CLIENT_SECRET) + + # 2. Proceed to Strava OAuth auth_url = ( f"https://www.strava.com/oauth/authorize" f"?client_id={CLIENT_ID}" @@ -137,69 +271,37 @@ def main(): f"&scope={SCOPES}" ) - # Configure handler CallbackHandler.client_id = CLIENT_ID CallbackHandler.client_secret = CLIENT_SECRET CallbackHandler.tokens = {} CallbackHandler.error = None - print("=" * 60) + print("\n" + "=" * 60) print(" Strava OAuth2 Authorization") print("=" * 60) - print(f"\nRequesting scopes: {SCOPES}\n") - print("Opening Strava in your browser...") - print("If the browser doesn't open, visit this URL manually:\n") - print(f" {auth_url}\n") + print("\nOpening Strava in your browser for final authentication...") webbrowser.open(auth_url) - print("Waiting for callback on http://localhost:8765 ...") + HTTPServer.allow_reuse_address = True server = HTTPServer(("localhost", 8765), CallbackHandler) - server.handle_request() # Handle exactly one request (the callback) - - if CallbackHandler.error: - print(f"❌ Token exchange failed: {CallbackHandler.error}") - return - - if not CallbackHandler.tokens: - print("❌ No tokens received.") - return - - data = CallbackHandler.tokens - refresh_token = data["refresh_token"] - athlete = data.get("athlete", {}) - - print("\n" + "=" * 60) - print(" ✅ Authorization successful!") - print("=" * 60) - print(f"\nAthlete: {athlete.get('firstname')} {athlete.get('lastname')}") - print(f"Scopes granted: {data.get('scope', 'unknown')}\n") - print("Add the following to your .env file:") - print("-" * 40) - print(f"STRAVA_CLIENT_ID={CLIENT_ID}") - print(f"STRAVA_CLIENT_SECRET={CLIENT_SECRET}") - print(f"STRAVA_REFRESH_TOKEN={refresh_token}") - print("-" * 40) - - # Optional: Automatically update .env if it exists try: - env_path = ".env" - if os.path.exists(env_path): - with open(env_path, "r") as f: - lines = f.readlines() - with open(env_path, "w") as f: - found = False - for line in lines: - if line.startswith("STRAVA_REFRESH_TOKEN="): - f.write(f"STRAVA_REFRESH_TOKEN={refresh_token}\n") - found = True - else: - f.write(line) - if not found: - f.write(f"\nSTRAVA_REFRESH_TOKEN={refresh_token}\n") - print("Successfully updated your .env file!") - except Exception as e: - print(f"Could not automatically update .env: {e}") + print("Waiting for Strava callback...") + while not CallbackHandler.tokens and not CallbackHandler.error: + server.handle_request() + finally: + server.server_close() + + if not CallbackHandler.error and CallbackHandler.tokens: + data = CallbackHandler.tokens + refresh_token = data.get("refresh_token") + if refresh_token: + save_to_env(CLIENT_ID, CLIENT_SECRET, refresh_token) + print("\n✅ Setup successful! All tokens saved to .env") + else: + print("\n❌ Error: No refresh token in response.") + elif CallbackHandler.error: + print(f"\n❌ Error: {CallbackHandler.error}") if __name__ == "__main__": diff --git a/src/strava_mcp_server/main.py b/src/strava_mcp_server/main.py index bbde414..4646540 100644 --- a/src/strava_mcp_server/main.py +++ b/src/strava_mcp_server/main.py @@ -58,7 +58,7 @@ def main() -> None: ) try: - mcp._mcp_server.version = importlib.metadata.version("strava-mcp-server") + mcp._mcp_server.version = importlib.metadata.version("strava-mcp-server-hnrx") except importlib.metadata.PackageNotFoundError: mcp._mcp_server.version = "dev" diff --git a/src/strava_mcp_server/tools/__init__.py b/src/strava_mcp_server/tools/__init__.py index 5eb2d29..85c954c 100644 --- a/src/strava_mcp_server/tools/__init__.py +++ b/src/strava_mcp_server/tools/__init__.py @@ -15,6 +15,7 @@ from . import segments from . import segment_efforts from . import gear from . import prompts +from . import server_info def register_tools(mcp: FastMCP, strava: StravaClient) -> None: @@ -27,3 +28,4 @@ def register_tools(mcp: FastMCP, strava: StravaClient) -> None: segment_efforts.register(mcp, strava) gear.register(mcp, strava) prompts.register(mcp, strava) + server_info.register(mcp, strava) diff --git a/src/strava_mcp_server/tools/server_info.py b/src/strava_mcp_server/tools/server_info.py new file mode 100644 index 0000000..18ac0aa --- /dev/null +++ b/src/strava_mcp_server/tools/server_info.py @@ -0,0 +1,106 @@ +""" +Server Info Tool + +Exposes server version, capabilities, and metadata as an MCP tool +so that LLM clients can query the server's current state. +""" + +import importlib.metadata +import os +from datetime import datetime, timezone + +from mcp.server.fastmcp import FastMCP +from mcp.types import EmbeddedResource, TextResourceContents + +from strava_mcp_server.strava_client import StravaClient + + +def _get_version() -> str: + try: + return importlib.metadata.version("strava-mcp-server-hnrx") + except importlib.metadata.PackageNotFoundError: + return "dev" + + +def register(mcp: FastMCP, strava: StravaClient) -> None: + + @mcp.tool( + name="get_server_info", + description=( + "Returns metadata about this MCP server instance: version, available tools, " + "transport mode, and connection status. Call this first to understand the " + "server's capabilities and current configuration." + ), + ) + async def get_server_info() -> list: + version = _get_version() + transport = os.getenv("MCP_TRANSPORT", "stdio") + host = os.getenv("HOST", "0.0.0.0") + port = os.getenv("PORT", "8000") + has_token = bool(os.getenv("STRAVA_REFRESH_TOKEN")) + queried_at = datetime.now(timezone.utc).isoformat() + + # Human-readable summary + status = ( + "✅ Authenticated" if has_token else "⚠️ Unauthenticated (no REFRESH_TOKEN)" + ) + endpoint = f"http://{host}:{port}/mcp" if transport == "http" else "stdio" + + markdown = f"""## Strava MCP Server + +| Field | Value | +|---------------|-------------------------------| +| **Version** | `{version}` | +| **Transport** | `{transport}` → `{endpoint}` | +| **Status** | {status} | +| **Queried** | {queried_at} | + +### Available Tool Categories +- 🏃 **Athlete** – Profile, stats, heart rate zones +- 🚴 **Activities** – List, detail, laps, streams, comments +- 🏆 **Segments** – Popular segments, starred segments, efforts +- 🛣️ **Routes** – Athlete routes +- 🏟️ **Clubs** – Membership, club activities +- ⚙️ **Gear** – Bikes and shoes with mileage + +### Quick Links +- [Repository](https://git.hnrx.net/hnrx/strava-mcp-server) +- [Website](https://strava-mcp.web.s3.hnrx.net) +- [PyPI](https://pypi.org/project/strava-mcp-server-hnrx/) +""" + + data = { + "server": "strava-mcp-server-hnrx", + "version": version, + "transport": transport, + "endpoint": endpoint, + "authenticated": has_token, + "queried_at": queried_at, + "tool_categories": [ + "athlete", + "activities", + "segments", + "routes", + "clubs", + "gear", + ], + "links": { + "repository": "https://git.hnrx.net/hnrx/strava-mcp-server", + "website": "https://strava-mcp.web.s3.hnrx.net", + "pypi": "https://pypi.org/project/strava-mcp-server-hnrx/", + }, + } + + import json + + return [ + {"type": "text", "text": markdown}, + EmbeddedResource( + type="resource", + resource=TextResourceContents( + uri="strava://server/info", + mimeType="application/json", + text=json.dumps(data, indent=2), + ), + ), + ] diff --git a/website/index.html b/website/index.html index 416db2d..ba46a4f 100644 --- a/website/index.html +++ b/website/index.html @@ -66,6 +66,11 @@

Hardware Tracking

Manage your equipment and track the mileage of your bikes and shoes.

+
+ 🪄 +

Interactive Onboarding

+

Zero configuration needed. Our guided wizard sets up your API access in minutes.

+
@@ -104,9 +109,18 @@

Quick Start

-

Start the server locally with a single command via UV.

+

Get started in seconds with our interactive onboarding wizard.

+
- $ uv run strava-mcp-server + # 1. Authenticate & Setup + $ uvx --from strava-mcp-server-hnrx auth +

+ # 2. Run the Server + $ uvx --from strava-mcp-server-hnrx server +
+ +
+ View full Documentation
@@ -126,6 +140,7 @@ "hero-subtitle": "A production-ready Model Context Protocol (MCP) server that exposes the Strava API for AI agents and LLMs.", "btn-start": "Get Started", "btn-more": "Learn More", + "btn-docs": "View full Documentation", "features-title": "Features", "features-subtitle": "Comprehensive access to your training data through standardized MCP tools.", "feat-1-t": "Athlete Profiles", @@ -136,6 +151,8 @@ "feat-3-d": "Explore popular segments and your saved routes with all metadata.", "feat-4-t": "Hardware Tracking", "feat-4-d": "Manage your equipment and track the mileage of your bikes and shoes.", + "feat-5-t": "Interactive Onboarding", + "feat-5-d": "Zero configuration needed. Our guided wizard sets up your API access in minutes.", "arch-title": "Dual-Output Architecture", "arch-p": "Optimized for both humans and machines. Every tool delivers two outputs:", "arch-user-t": "User Content:", @@ -143,7 +160,9 @@ "arch-llm-t": "Assistant Resource:", "arch-llm-d": "Structured JSON for precise data processing by the LLM.", "install-title": "Quick Start", - "install-p": "Start the server locally with a single command via UV." + "install-p": "Get started in seconds with our interactive onboarding wizard.", + "install-auth": "uvx --from strava-mcp-server-hnrx auth", + "install-cmd": "uvx --from strava-mcp-server-hnrx server" }, de: { "nav-features": "Funktionen", @@ -154,6 +173,7 @@ "hero-subtitle": "Ein produktionsreifer Model Context Protocol (MCP) Server, der die Strava API für AI Agents und LLMs nutzbar macht.", "btn-start": "Loslegen", "btn-more": "Mehr erfahren", + "btn-docs": "Gesamte Dokumentation", "features-title": "Funktionen", "features-subtitle": "Umfangreicher Zugriff auf deine Trainingsdaten über standardisierte MCP Tools.", "feat-1-t": "Athleten-Profile", @@ -164,6 +184,8 @@ "feat-3-d": "Erkunde beliebte Segmente und deine gespeicherten Routen mit allen Metadaten.", "feat-4-t": "Hardware-Tracking", "feat-4-d": "Verwalte deine Ausrüstung und verfolge die Laufleistung deiner Bikes und Schuhe.", + "feat-5-t": "Interaktives Onboarding", + "feat-5-d": "Keine manuelle Konfiguration nötig. Unser Wizard richtet deinen API-Zugang in Minuten ein.", "arch-title": "Dual-Output Architektur", "arch-p": "Optimiert für Mensch und Maschine. Jedes Tool liefert zwei Ausgaben:", "arch-user-t": "User Content:", @@ -171,7 +193,9 @@ "arch-llm-t": "Assistant Resource:", "arch-llm-d": "Strukturiertes JSON für präzise Datenverarbeitung durch das LLM.", "install-title": "Schnellstart", - "install-p": "Starte den Server lokal mit nur einem Befehl über UV." + "install-p": "Starte in Sekunden mit unserem interaktiven Onboarding-Wizard.", + "install-auth": "uvx --from strava-mcp-server-hnrx auth", + "install-cmd": "uvx --from strava-mcp-server-hnrx server" } };