Files
strava-mcp-server/src/strava_mcp_server/get_token.py
T
matthias 6db9e87f96
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
feat: add server_info tool for diagnostics and implement interactive CLI onboarding wizard for easier authentication
2026-05-14 21:12:48 +02:00

309 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Strava OAuth2 Authorization Helper
This script guides you through the Strava OAuth2 flow to obtain a refresh token
with the correct scopes for the MCP server.
Required scopes:
- read → basic athlete profile
- activity:read → read your activities
- activity:read_all → read private activities (optional)
Usage: uv run get_token.py
"""
import os
import webbrowser
import httpx
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.getenv("STRAVA_CLIENT_ID")
CLIENT_SECRET = os.getenv("STRAVA_CLIENT_SECRET")
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(
"""
<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):
client_id: str = ""
client_secret: str = ""
tokens: dict = {}
error: str | None = None
def do_GET(self):
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
if "code" in params:
code = params["code"][0]
try:
# Exchange code for token synchronously
response = httpx.post(
"https://www.strava.com/oauth/token",
data={
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"grant_type": "authorization_code",
},
)
response.raise_for_status()
CallbackHandler.tokens = response.json()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
refresh_token = CallbackHandler.tokens.get("refresh_token")
self.wfile.write(
f"""
<html>
<head>
<style>
body {{ background: #0A0A0A; color: white; font-family: -apple-system, sans-serif; max-width: 600px; margin: 60px auto; padding: 20px; text-align: center; }}
.card {{ background: #161616; border-radius: 16px; padding: 30px; border: 1px solid rgba(255,255,255,0.1); text-align: left; margin-top: 30px; }}
h2 {{ color: #fc4c02; }}
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 {{ font-size: 64px; margin-bottom: 20px; display: block; }}
</style>
</head>
<body>
<span class="success-icon">✅</span>
<h2>Setup Complete!</h2>
<p>Your Strava account is now connected to the MCP server.</p>
<div class="card">
<p style="margin-top:0; color: #A0A0A0; font-size: 14px; font-weight: bold;">UPDATED .ENV CONTENT:</p>
<pre>STRAVA_CLIENT_ID={self.client_id}
STRAVA_CLIENT_SECRET={self.client_secret}
STRAVA_REFRESH_TOKEN={refresh_token}</pre>
<p style="font-size: 13px; color: #666; margin-bottom: 0;">This information has been automatically saved to your .env file.</p>
</div>
<p style="margin-top: 40px; color: #444;">You can now close this window and restart the server.</p>
</body>
</html>
""".encode("utf-8")
)
except Exception as e:
CallbackHandler.error = str(e)
self.send_response(500)
self.end_headers()
self.wfile.write(f"Error exchanging token: {e}".encode())
else:
error_msg = params.get("error", ["unknown"])[0]
self.send_response(400)
self.end_headers()
self.wfile.write(f"Error: {error_msg}".encode())
def log_message(self, format, *args):
pass # Suppress server logs
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}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&approval_prompt=force"
f"&scope={SCOPES}"
)
CallbackHandler.client_id = CLIENT_ID
CallbackHandler.client_secret = CLIENT_SECRET
CallbackHandler.tokens = {}
CallbackHandler.error = None
print("\n" + "=" * 60)
print(" Strava OAuth2 Authorization")
print("=" * 60)
print("\nOpening Strava in your browser for final authentication...")
webbrowser.open(auth_url)
HTTPServer.allow_reuse_address = True
server = HTTPServer(("localhost", 8765), CallbackHandler)
try:
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__":
main()