diff --git a/pyproject.toml b/pyproject.toml
index c10f8aa..15a3301 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,8 @@ Repository = "https://git.hnrx.net/hnrx/strava-mcp-server"
[project.scripts]
strava-mcp = "strava_mcp_server.main:main"
strava-mcp-get-token = "strava_mcp_server.get_token:main"
+server = "strava_mcp_server.main:main"
+auth = "strava_mcp_server.get_token:main"
[dependency-groups]
dev = [
diff --git a/src/strava_mcp_server/get_token.py b/src/strava_mcp_server/get_token.py
index 5c8fa60..12a2bf2 100644
--- a/src/strava_mcp_server/get_token.py
+++ b/src/strava_mcp_server/get_token.py
@@ -25,33 +25,95 @@ CLIENT_SECRET = os.getenv("STRAVA_CLIENT_SECRET")
REDIRECT_URI = "http://localhost:8765/callback"
SCOPES = "profile:read_all,activity:read_all,activity:read,profile:write"
-# Global to capture the auth code from the callback
-auth_code: str | None = None
-
-
class CallbackHandler(BaseHTTPRequestHandler):
+ client_id: str = ""
+ client_secret: str = ""
+ tokens: dict = {}
+ error: str | None = None
+
def do_GET(self):
- global auth_code
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
if "code" in params:
- auth_code = params["code"][0]
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.end_headers()
- self.wfile.write(b"""
-
- ✅ Authorization successful!
- You can close this window and return to your terminal.
-
- """)
+ 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()
+ self.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")
+
+ self.wfile.write(f"""
+
+
+
+
+
+
+
+ 1. Local Setup (.env)
+ Copy the following block into your .env file in the project root:
+
+
Your .env content:
+
STRAVA_CLIENT_ID={self.client_id}
+STRAVA_CLIENT_SECRET={self.client_secret}
+STRAVA_REFRESH_TOKEN={refresh_token}
+
+
+ 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 —
+
+
+
+ """.encode("utf-8"))
+ except Exception as e:
+ self.error = str(e)
+ self.send_response(500)
+ self.end_headers()
+ self.wfile.write(f"Error exchanging token: {e}".encode())
else:
- error = params.get("error", ["unknown"])[0]
+ error_msg = params.get("error", ["unknown"])[0]
self.send_response(400)
- self.send_header("Content-Type", "text/html")
self.end_headers()
- self.wfile.write(f"❌ Error: {error}
".encode())
+ self.wfile.write(f"Error: {error_msg}".encode())
def log_message(self, format, *args):
pass # Suppress server logs
@@ -71,6 +133,12 @@ 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(" Strava OAuth2 Authorization")
print("=" * 60)
@@ -85,26 +153,15 @@ def main():
server = HTTPServer(("localhost", 8765), CallbackHandler)
server.handle_request() # Handle exactly one request (the callback)
- if not auth_code:
- print("❌ No authorization code received.")
+ if CallbackHandler.error:
+ print(f"❌ Token exchange failed: {CallbackHandler.error}")
return
- print("\nExchanging authorization code for tokens...")
- response = httpx.post(
- "https://www.strava.com/oauth/token",
- data={
- "client_id": CLIENT_ID,
- "client_secret": CLIENT_SECRET,
- "code": auth_code,
- "grant_type": "authorization_code",
- },
- )
-
- if response.status_code != 200:
- print(f"❌ Token exchange failed: {response.status_code} {response.text}")
+ if not CallbackHandler.tokens:
+ print("❌ No tokens received.")
return
- data = response.json()
+ data = CallbackHandler.tokens
refresh_token = data["refresh_token"]
athlete = data.get("athlete", {})
@@ -119,6 +176,26 @@ def main():
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}")
if __name__ == "__main__":
diff --git a/src/strava_mcp_server/tools/auth.py b/src/strava_mcp_server/tools/auth.py
index 984d13d..8c02a5b 100644
--- a/src/strava_mcp_server/tools/auth.py
+++ b/src/strava_mcp_server/tools/auth.py
@@ -7,31 +7,96 @@ from urllib.parse import urlparse, parse_qs
from mcp.server.fastmcp import FastMCP, Context
from mcp.types import TextContent
-auth_code: str | None = None
-
class CallbackHandler(BaseHTTPRequestHandler):
+ client_id: str = ""
+ client_secret: str = ""
+ tokens: dict = {}
+ error: str | None = None
+
def do_GET(self):
- global auth_code
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
if "code" in params:
- auth_code = params["code"][0]
- self.send_response(200)
- self.send_header("Content-Type", "text/html")
- self.end_headers()
- self.wfile.write(b"""
-
- ✅ Authorization successful!
- You can close this window and return to your terminal/chat.
-
- """)
+ code = params["code"][0]
+ try:
+ # Exchange code for token synchronously inside the handler
+ import httpx
+ 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()
+ self.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")
+
+ self.wfile.write(f"""
+
+
+
+
+
+
+
+ 1. Local Setup (.env)
+ Copy the following block into your .env file in the project root:
+
+
Your .env content:
+
STRAVA_CLIENT_ID={self.client_id}
+STRAVA_CLIENT_SECRET={self.client_secret}
+STRAVA_REFRESH_TOKEN={refresh_token}
+
+
+ 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 —
+
+
+
+ """.encode("utf-8"))
+ except Exception as e:
+ self.error = str(e)
+ self.send_response(500)
+ self.end_headers()
+ self.wfile.write(f"Error exchanging token: {e}".encode())
else:
- error = params.get("error", ["unknown"])[0]
+ error_msg = params.get("error", ["unknown"])[0]
self.send_response(400)
- self.send_header("Content-Type", "text/html")
self.end_headers()
- self.wfile.write(f"❌ Error: {error}
".encode())
+ self.wfile.write(f"Error: {error_msg}".encode())
def log_message(self, format, *args):
pass # Suppress server logs
@@ -44,14 +109,17 @@ def register(mcp: FastMCP, strava) -> None:
This opens a browser window for the user to log in and authorize the app.
It then intercepts the redirect locally, obtains the token, and returns the tokens.
"""
- global auth_code
- auth_code = None
-
client_id = os.getenv("STRAVA_CLIENT_ID")
client_secret = os.getenv("STRAVA_CLIENT_SECRET")
if not client_id or not client_secret:
return [TextContent(type="text", text="Error: Missing STRAVA_CLIENT_ID or STRAVA_CLIENT_SECRET in .env")]
+ # Configure handler with credentials
+ CallbackHandler.client_id = client_id
+ CallbackHandler.client_secret = client_secret
+ CallbackHandler.tokens = {}
+ CallbackHandler.error = None
+
redirect_uri = "http://localhost:8765/callback"
scopes = "profile:read_all,activity:read_all,activity:read,profile:write"
@@ -73,27 +141,15 @@ def register(mcp: FastMCP, strava) -> None:
# Run handle_request in a separate thread so it doesn't block the async event loop
await asyncio.to_thread(server.handle_request)
- if not auth_code:
- return [TextContent(type="text", text="Error: No authorization code received.")]
+ if CallbackHandler.error:
+ return [TextContent(type="text", text=f"Error during token exchange: {CallbackHandler.error}")]
- await ctx.info("Authorization code received. Exchanging for tokens...")
+ if not CallbackHandler.tokens:
+ return [TextContent(type="text", text="Error: No tokens received.")]
+
+ await ctx.info("Tokens received successfully.")
- async with httpx.AsyncClient() as client:
- response = await client.post(
- "https://www.strava.com/oauth/token",
- data={
- "client_id": client_id,
- "client_secret": client_secret,
- "code": auth_code,
- "grant_type": "authorization_code",
- },
- )
-
- if response.status_code != 200:
- return [TextContent(type="text", text=f"Error: Token exchange failed: {response.status_code} {response.text}")]
-
- data = response.json()
- refresh_token = data.get("refresh_token")
+ refresh_token = CallbackHandler.tokens.get("refresh_token")
# Update the .env file if it exists
env_msg = ""
@@ -103,11 +159,15 @@ def register(mcp: FastMCP, strava) -> None:
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")
env_msg = "\nI have also automatically updated your .env file with the new refresh token!"
except Exception as e:
env_msg = f"\nFailed to automatically update .env file: {e}"