3 Commits

Author SHA1 Message Date
matthias d5487c07fc docs: update docker installation instructions with automated image registry and config management details
CI/CD Pipeline / Lint & Check (push) Successful in 10s
CI/CD Pipeline / Publish to PyPI (push) Successful in 12s
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m23s
2026-05-14 21:36:13 +02:00
matthias b8bce4ee7f feat: migrate credential storage to platform-specific configuration directories
CI/CD Pipeline / Lint & Check (push) Successful in 10s
CI/CD Pipeline / Publish to PyPI (push) Has been skipped
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m18s
2026-05-14 21:29:18 +02:00
matthias 6db9e87f96 feat: add server_info tool for diagnostics and implement interactive CLI onboarding wizard for easier authentication
CI/CD Pipeline / Lint & Check (push) Successful in 10s
Deploy Website to S3 / deploy (push) Successful in 6s
CI/CD Pipeline / Publish to PyPI (push) Has been skipped
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m19s
2026-05-14 21:12:48 +02:00
9 changed files with 456 additions and 121 deletions
+68 -24
View File
@@ -49,25 +49,80 @@ Built with [FastMCP](https://github.com/jlowin/fastmcp) and the [MCP Python SDK]
## Installation & Deployment ## Installation & Deployment
### Docker (Recommended) ### Docker (Recommended for self-hosting)
The project includes a multi-arch Docker build (amd64/arm64). A pre-built multi-arch image (amd64/arm64) is automatically published to the Gitea registry on every release.
**1. Authenticate first** (only needed once, saves credentials to `~/.config/strava-mcp-server/config.env`):
```bash ```bash
# Clone the repository uvx --from strava-mcp-server-hnrx auth
git clone https://git.hnrx.net/hnrx/strava-mcp-server.git
cd strava-mcp-server
# Build the image locally
docker build -t strava-mcp-server:latest .
# Run the container (injecting your .env file)
docker run --rm -p 8000:8000 --env-file .env strava-mcp-server:latest
``` ```
### Local Python (uv) **2. Pull and run the image:**
We use `uv` for lightning-fast dependency management and task execution. ```bash
# Pull the latest release
docker pull git.hnrx.net/hnrx/strava-mcp-server:latest
# Run with your credentials from the config file
docker run --rm -p 8000:8000 \
-e STRAVA_CLIENT_ID=<your_client_id> \
-e STRAVA_CLIENT_SECRET=<your_client_secret> \
-e STRAVA_REFRESH_TOKEN=<your_refresh_token> \
-e MCP_TRANSPORT=http \
git.hnrx.net/hnrx/strava-mcp-server:latest
```
Or using your config file directly:
```bash
docker run --rm -p 8000:8000 \
--env-file ~/.config/strava-mcp-server/config.env \
-e MCP_TRANSPORT=http \
git.hnrx.net/hnrx/strava-mcp-server:latest
```
> **Tip:** Pin to a specific version for stability, e.g. `git.hnrx.net/hnrx/strava-mcp-server:v0.2.0`
<details>
<summary>Build from source</summary>
```bash
git clone https://git.hnrx.net/hnrx/strava-mcp-server.git
cd strava-mcp-server
docker build -t strava-mcp-server:latest .
docker run --rm -p 8000:8000 --env-file ~/.config/strava-mcp-server/config.env -e MCP_TRANSPORT=http strava-mcp-server:latest
```
</details>
### Local Python (PyPI)
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 ```bash
git clone https://git.hnrx.net/hnrx/strava-mcp-server.git git clone https://git.hnrx.net/hnrx/strava-mcp-server.git
@@ -80,17 +135,6 @@ uv run server
uv run auth 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 ## Strava API Setup
+3
View File
@@ -0,0 +1,3 @@
STRAVA_CLIENT_ID=16037
STRAVA_CLIENT_SECRET=cc332cb8b0f7f44dac80100be87495a0a1440a2d
STRAVA_REFRESH_TOKEN=bb644951ca96e811f9520794c607a0e9b6505888
+1 -1
View File
@@ -33,7 +33,7 @@ dependencies = [
] ]
[project.urls] [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" Repository = "https://git.hnrx.net/hnrx/strava-mcp-server"
"Bug Tracker" = "https://git.hnrx.net/hnrx/strava-mcp-server/issues" "Bug Tracker" = "https://git.hnrx.net/hnrx/strava-mcp-server/issues"
+38
View File
@@ -0,0 +1,38 @@
"""
Configuration path resolution for strava-mcp-server.
Config is stored in the appropriate user config directory per platform:
- macOS/Linux: ~/.config/strava-mcp-server/config.env (XDG convention)
- Windows: %APPDATA%\\strava-mcp-server\\config.env
A local .env file (if present) always takes precedence for developer overrides.
"""
import os
import sys
from pathlib import Path
APP_NAME = "strava-mcp-server"
def get_config_dir() -> Path:
"""Returns the appropriate config directory for the current platform."""
if sys.platform == "win32":
# Windows: use %APPDATA% (C:\Users\<user>\AppData\Roaming\)
appdata = os.environ.get("APPDATA")
if appdata:
return Path(appdata) / APP_NAME
# macOS / Linux: XDG convention (~/.config)
return Path.home() / ".config" / APP_NAME
def get_config_file() -> Path:
"""Returns the path to the config file."""
return get_config_dir() / "config.env"
def ensure_config_dir() -> Path:
"""Creates the config directory if it doesn't exist and returns its path."""
config_dir = get_config_dir()
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
+203 -90
View File
@@ -19,7 +19,13 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() from strava_mcp_server.config import get_config_file, ensure_config_dir
# Load config: platform config dir first, then local .env (local overrides)
load_dotenv(
get_config_file()
) # e.g. ~/Library/Application Support/strava-mcp-server/config.env
load_dotenv(override=False) # local .env overrides if present
CLIENT_ID = os.getenv("STRAVA_CLIENT_ID") CLIENT_ID = os.getenv("STRAVA_CLIENT_ID")
CLIENT_SECRET = os.getenv("STRAVA_CLIENT_SECRET") CLIENT_SECRET = os.getenv("STRAVA_CLIENT_SECRET")
@@ -27,11 +33,107 @@ REDIRECT_URI = "http://localhost:8765/callback"
SCOPES = "profile:read_all,activity:read_all,activity:read,profile:write" 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(
"""
<html>
<head><meta http-equiv="refresh" content="2;url=/callback-wait"></head>
<body style="background:#0A0A0A;color:white;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;">
<div style="text-align:center;">
<h2 style="color:#fc4c02;">Settings Saved!</h2>
<p>Redirecting to Strava Authorization...</p>
</div>
</body>
</html>
""".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 """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Strava MCP Setup</title>
<style>
:root { --primary: #FC4C02; --bg: #0A0A0A; --card: #161616; --text: #FFFFFF; --text-dim: #A0A0A0; }
body { font-family: -apple-system, system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.container { max-width: 500px; width: 90%; background: var(--card); padding: 40px; border-radius: 24px; box-shadow: 0 20px 40px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.1); }
h1 { color: var(--primary); margin-top: 0; font-size: 28px; }
.guide { background: rgba(252, 76, 2, 0.1); padding: 20px; border-radius: 12px; margin-bottom: 30px; font-size: 14px; line-height: 1.5; border-left: 4px solid var(--primary); }
.guide ol { margin: 10px 0 0 20px; padding: 0; }
.guide li { margin-bottom: 8px; }
label { display: block; margin-bottom: 8px; font-weight: 600; font-size: 14px; color: var(--text-dim); }
input { width: 100%; padding: 12px 16px; background: #222; border: 1px solid #333; border-radius: 8px; color: white; margin-bottom: 20px; box-sizing: border-box; font-family: monospace; }
input:focus { border-color: var(--primary); outline: none; }
button { width: 100%; padding: 14px; background: var(--primary); color: white; border: none; border-radius: 8px; font-weight: 700; cursor: pointer; transition: transform 0.2s; font-size: 16px; }
button:hover { transform: translateY(-2px); filter: brightness(1.1); }
code { background: #000; padding: 2px 6px; border-radius: 4px; font-family: monospace; color: var(--primary); }
</style>
</head>
<body>
<div class="container">
<h1>Strava MCP Setup</h1>
<div class="guide">
<strong>How to get your credentials:</strong>
<ol>
<li>Go to <a href="https://www.strava.com/settings/api" target="_blank" style="color:var(--primary);">Strava API Settings</a>.</li>
<li>Create an app (any name/category).</li>
<li>Set <b>"Authorization Callback Domain"</b> to <code>localhost</code>.</li>
<li>Copy your <b>Client ID</b> and <b>Client Secret</b> below.</li>
</ol>
</div>
<form action="/save" method="get">
<label>Client ID</label>
<input type="text" name="id" placeholder="e.g. 123456" required>
<label>Client Secret</label>
<input type="password" name="secret" placeholder="Your Strava Secret" required>
<button type="submit">Save & Authenticate</button>
</form>
</div>
</body>
</html>
"""
def log_message(self, format, *args):
pass
class CallbackHandler(BaseHTTPRequestHandler): class CallbackHandler(BaseHTTPRequestHandler):
client_id: str = "" client_id: str = ""
client_secret: str = "" client_secret: str = ""
tokens: dict = {} tokens: dict = {}
error: str | None = None error: str | None = None
config_path: str = ""
def do_GET(self): def do_GET(self):
parsed = urlparse(self.path) parsed = urlparse(self.path)
@@ -51,65 +153,46 @@ class CallbackHandler(BaseHTTPRequestHandler):
}, },
) )
response.raise_for_status() response.raise_for_status()
self.tokens = response.json() CallbackHandler.tokens = response.json()
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers() self.end_headers()
refresh_token = self.tokens.get("refresh_token") refresh_token = CallbackHandler.tokens.get("refresh_token")
self.wfile.write( self.wfile.write(
f""" f"""
<html> <html>
<head> <head>
<style> <style>
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 40px auto; padding: 20px; text-align: left; }} body {{ background: #0A0A0A; color: white; font-family: -apple-system, sans-serif; max-width: 600px; margin: 60px auto; padding: 20px; text-align: center; }}
.card {{ background: #f4f7f6; border-radius: 8px; padding: 20px; border-left: 5px solid #fc4c02; margin-top: 20px; }} .card {{ background: #161616; border-radius: 16px; padding: 30px; border: 1px solid rgba(255,255,255,0.1); text-align: left; margin-top: 30px; }}
pre {{ background: #222; color: #fff; padding: 15px; border-radius: 5px; overflow-x: auto; font-family: "SF Mono", Monaco, Consolas, monospace; font-size: 13px; }} h2 {{ color: #fc4c02; }}
.success-header {{ text-align: center; margin-bottom: 40px; }} pre {{ background: #000; color: #fc4c02; padding: 20px; border-radius: 8px; overflow-x: auto; font-family: monospace; font-size: 14px; border: 1px solid #333; }}
.success-icon {{ color: #2ecc71; font-size: 48px; display: block; margin-bottom: 10px; }} .success-icon {{ font-size: 64px; margin-bottom: 20px; display: block; }}
h2, h3 {{ color: #fc4c02; }}
.env-label {{ font-weight: bold; color: #666; font-size: 12px; text-transform: uppercase; margin-bottom: 5px; display: block; }}
.copy-hint {{ font-size: 12px; color: #666; font-style: italic; }}
code {{ background: #eee; padding: 2px 4px; border-radius: 4px; }}
</style> </style>
</head> </head>
<body> <body>
<div class="success-header"> <span class="success-icon">✅</span>
<span class="success-icon">&#x2705;</span> <h2>Setup Complete!</h2>
<h2>Authorization successful!</h2> <p>Your Strava account is now connected to the MCP server.</p>
<p>You have successfully authenticated with Strava. You can now close this window.</p>
</div>
<h3>1. Local Setup (.env)</h3>
<p>Copy the following block into your <code>.env</code> file in the project root:</p>
<div class="card"> <div class="card">
<span class="env-label">Your .env content:</span> <p style="margin-top:0; color: #A0A0A0; font-size: 14px; font-weight: bold;">UPDATED .ENV CONTENT:</p>
<pre>STRAVA_CLIENT_ID={self.client_id} <pre>STRAVA_CLIENT_ID={self.client_id}
STRAVA_CLIENT_SECRET={self.client_secret} STRAVA_CLIENT_SECRET={self.client_secret}
STRAVA_REFRESH_TOKEN={refresh_token}</pre> STRAVA_REFRESH_TOKEN={refresh_token}</pre>
<p style="font-size: 13px; color: #666; margin-bottom: 0;">Automatically saved to:</p>
<code style="font-size: 12px; color: #fc4c02; word-break: break-all;">{CallbackHandler.config_path}</code>
</div> </div>
<p style="margin-top: 40px; color: #444;">You can now close this window and restart the server.</p>
<h3>2. Kubernetes Setup (Secret)</h3>
<p>If you are deploying this server to Kubernetes, run the following command to create the required Secret:</p>
<div class="card">
<span class="env-label">Kubectl Command:</span>
<pre>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}</pre>
</div>
<p style="margin-top: 40px; font-size: 14px; color: #666; text-align: center;">
&mdash; Strava MCP Server Authorization Helper &mdash;
</p>
</body> </body>
</html> </html>
""".encode("utf-8") """.encode("utf-8")
) )
except Exception as e: except Exception as e:
self.error = str(e) CallbackHandler.error = str(e)
self.send_response(500) self.send_response(500)
self.end_headers() self.end_headers()
self.wfile.write(f"Error exchanging token: {e}".encode()) self.wfile.write(f"Error exchanging token: {e}".encode())
@@ -123,11 +206,72 @@ STRAVA_REFRESH_TOKEN={refresh_token}</pre>
pass # Suppress server logs pass # Suppress server logs
def main(): def save_to_env(client_id, client_secret, refresh_token=None):
if not CLIENT_ID or not CLIENT_SECRET: # Always save to the platform config directory so uvx finds it
print("❌ Missing STRAVA_CLIENT_ID or STRAVA_CLIENT_SECRET in .env") ensure_config_dir()
return config_path = get_config_file()
lines = []
if config_path.exists():
with open(config_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(config_path, "w") as f:
f.writelines(new_lines)
saved_keys = ", ".join(keys_to_set.keys())
print(f"📝 Saved to {config_path}: {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 = ( auth_url = (
f"https://www.strava.com/oauth/authorize" f"https://www.strava.com/oauth/authorize"
f"?client_id={CLIENT_ID}" f"?client_id={CLIENT_ID}"
@@ -137,69 +281,38 @@ def main():
f"&scope={SCOPES}" f"&scope={SCOPES}"
) )
# Configure handler
CallbackHandler.client_id = CLIENT_ID CallbackHandler.client_id = CLIENT_ID
CallbackHandler.client_secret = CLIENT_SECRET CallbackHandler.client_secret = CLIENT_SECRET
CallbackHandler.tokens = {} CallbackHandler.tokens = {}
CallbackHandler.error = None CallbackHandler.error = None
CallbackHandler.config_path = str(get_config_file())
print("=" * 60) print("\n" + "=" * 60)
print(" Strava OAuth2 Authorization") print(" Strava OAuth2 Authorization")
print("=" * 60) print("=" * 60)
print(f"\nRequesting scopes: {SCOPES}\n") print("\nOpening Strava in your browser for final authentication...")
print("Opening Strava in your browser...")
print("If the browser doesn't open, visit this URL manually:\n")
print(f" {auth_url}\n")
webbrowser.open(auth_url) webbrowser.open(auth_url)
print("Waiting for callback on http://localhost:8765 ...") HTTPServer.allow_reuse_address = True
server = HTTPServer(("localhost", 8765), CallbackHandler) 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: try:
env_path = ".env" print("Waiting for Strava callback...")
if os.path.exists(env_path): while not CallbackHandler.tokens and not CallbackHandler.error:
with open(env_path, "r") as f: server.handle_request()
lines = f.readlines() finally:
with open(env_path, "w") as f: server.server_close()
found = False
for line in lines: if not CallbackHandler.error and CallbackHandler.tokens:
if line.startswith("STRAVA_REFRESH_TOKEN="): data = CallbackHandler.tokens
f.write(f"STRAVA_REFRESH_TOKEN={refresh_token}\n") refresh_token = data.get("refresh_token")
found = True if refresh_token:
else: save_to_env(CLIENT_ID, CLIENT_SECRET, refresh_token)
f.write(line) print("\n✅ Setup successful! All tokens saved to .env")
if not found: else:
f.write(f"\nSTRAVA_REFRESH_TOKEN={refresh_token}\n") print("\n❌ Error: No refresh token in response.")
print("Successfully updated your .env file!") elif CallbackHandler.error:
except Exception as e: print(f"\n❌ Error: {CallbackHandler.error}")
print(f"Could not automatically update .env: {e}")
if __name__ == "__main__": if __name__ == "__main__":
+7 -2
View File
@@ -5,10 +5,15 @@ import sys
from dotenv import load_dotenv from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from strava_mcp_server.config import get_config_file
from strava_mcp_server.strava_client import StravaClient from strava_mcp_server.strava_client import StravaClient
from strava_mcp_server.tools import register_tools from strava_mcp_server.tools import register_tools
load_dotenv() # Load credentials: platform config dir first, then local .env (local overrides)
load_dotenv(
get_config_file()
) # ~/Library/Application Support/strava-mcp-server/config.env
load_dotenv(override=False) # local .env overrides if present
def validate_credentials() -> None: def validate_credentials() -> None:
@@ -58,7 +63,7 @@ def main() -> None:
) )
try: 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: except importlib.metadata.PackageNotFoundError:
mcp._mcp_server.version = "dev" mcp._mcp_server.version = "dev"
+2
View File
@@ -15,6 +15,7 @@ from . import segments
from . import segment_efforts from . import segment_efforts
from . import gear from . import gear
from . import prompts from . import prompts
from . import server_info
def register_tools(mcp: FastMCP, strava: StravaClient) -> None: 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) segment_efforts.register(mcp, strava)
gear.register(mcp, strava) gear.register(mcp, strava)
prompts.register(mcp, strava) prompts.register(mcp, strava)
server_info.register(mcp, strava)
+106
View File
@@ -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),
),
),
]
+28 -4
View File
@@ -66,6 +66,11 @@
<h3 data-i18n="feat-4-t">Hardware Tracking</h3> <h3 data-i18n="feat-4-t">Hardware Tracking</h3>
<p data-i18n="feat-4-d">Manage your equipment and track the mileage of your bikes and shoes.</p> <p data-i18n="feat-4-d">Manage your equipment and track the mileage of your bikes and shoes.</p>
</div> </div>
<div class="card">
<span class="card-icon">🪄</span>
<h3 data-i18n="feat-5-t">Interactive Onboarding</h3>
<p data-i18n="feat-5-d">Zero configuration needed. Our guided wizard sets up your API access in minutes.</p>
</div>
</div> </div>
</section> </section>
@@ -104,9 +109,18 @@
<section id="installation" class="quick-start"> <section id="installation" class="quick-start">
<h2 data-i18n="install-title">Quick Start</h2> <h2 data-i18n="install-title">Quick Start</h2>
<p data-i18n="install-p">Start the server locally with a single command via UV.</p> <p data-i18n="install-p">Get started in seconds with our interactive onboarding wizard.</p>
<div class="terminal"> <div class="terminal">
<span>$</span> uv run strava-mcp-server <span class="comment"># 1. Authenticate & Setup</span>
<span>$</span> <code data-i18n="install-auth">uvx --from strava-mcp-server-hnrx auth</code>
<br><br>
<span class="comment"># 2. Run the Server</span>
<span>$</span> <code data-i18n="install-cmd">uvx --from strava-mcp-server-hnrx server</code>
</div>
<div style="margin-top: 2rem; text-align: center;">
<a href="https://git.hnrx.net/hnrx/strava-mcp-server" class="btn btn-secondary" style="font-size: 0.9rem;" data-i18n="btn-docs">View full Documentation</a>
</div> </div>
</section> </section>
@@ -126,6 +140,7 @@
"hero-subtitle": "A production-ready Model Context Protocol (MCP) server that exposes the Strava API for AI agents and LLMs.", "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-start": "Get Started",
"btn-more": "Learn More", "btn-more": "Learn More",
"btn-docs": "View full Documentation",
"features-title": "Features", "features-title": "Features",
"features-subtitle": "Comprehensive access to your training data through standardized MCP tools.", "features-subtitle": "Comprehensive access to your training data through standardized MCP tools.",
"feat-1-t": "Athlete Profiles", "feat-1-t": "Athlete Profiles",
@@ -136,6 +151,8 @@
"feat-3-d": "Explore popular segments and your saved routes with all metadata.", "feat-3-d": "Explore popular segments and your saved routes with all metadata.",
"feat-4-t": "Hardware Tracking", "feat-4-t": "Hardware Tracking",
"feat-4-d": "Manage your equipment and track the mileage of your bikes and shoes.", "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-title": "Dual-Output Architecture",
"arch-p": "Optimized for both humans and machines. Every tool delivers two outputs:", "arch-p": "Optimized for both humans and machines. Every tool delivers two outputs:",
"arch-user-t": "User Content:", "arch-user-t": "User Content:",
@@ -143,7 +160,9 @@
"arch-llm-t": "Assistant Resource:", "arch-llm-t": "Assistant Resource:",
"arch-llm-d": "Structured JSON for precise data processing by the LLM.", "arch-llm-d": "Structured JSON for precise data processing by the LLM.",
"install-title": "Quick Start", "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: { de: {
"nav-features": "Funktionen", "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.", "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-start": "Loslegen",
"btn-more": "Mehr erfahren", "btn-more": "Mehr erfahren",
"btn-docs": "Gesamte Dokumentation",
"features-title": "Funktionen", "features-title": "Funktionen",
"features-subtitle": "Umfangreicher Zugriff auf deine Trainingsdaten über standardisierte MCP Tools.", "features-subtitle": "Umfangreicher Zugriff auf deine Trainingsdaten über standardisierte MCP Tools.",
"feat-1-t": "Athleten-Profile", "feat-1-t": "Athleten-Profile",
@@ -164,6 +184,8 @@
"feat-3-d": "Erkunde beliebte Segmente und deine gespeicherten Routen mit allen Metadaten.", "feat-3-d": "Erkunde beliebte Segmente und deine gespeicherten Routen mit allen Metadaten.",
"feat-4-t": "Hardware-Tracking", "feat-4-t": "Hardware-Tracking",
"feat-4-d": "Verwalte deine Ausrüstung und verfolge die Laufleistung deiner Bikes und Schuhe.", "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-title": "Dual-Output Architektur",
"arch-p": "Optimiert für Mensch und Maschine. Jedes Tool liefert zwei Ausgaben:", "arch-p": "Optimiert für Mensch und Maschine. Jedes Tool liefert zwei Ausgaben:",
"arch-user-t": "User Content:", "arch-user-t": "User Content:",
@@ -171,7 +193,9 @@
"arch-llm-t": "Assistant Resource:", "arch-llm-t": "Assistant Resource:",
"arch-llm-d": "Strukturiertes JSON für präzise Datenverarbeitung durch das LLM.", "arch-llm-d": "Strukturiertes JSON für präzise Datenverarbeitung durch das LLM.",
"install-title": "Schnellstart", "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"
} }
}; };