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

This commit is contained in:
2026-05-14 21:29:18 +02:00
parent 6db9e87f96
commit b8bce4ee7f
4 changed files with 66 additions and 9 deletions
+3
View File
@@ -0,0 +1,3 @@
STRAVA_CLIENT_ID=16037
STRAVA_CLIENT_SECRET=cc332cb8b0f7f44dac80100be87495a0a1440a2d
STRAVA_REFRESH_TOKEN=bb644951ca96e811f9520794c607a0e9b6505888
+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
+19 -8
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")
@@ -127,6 +133,7 @@ class CallbackHandler(BaseHTTPRequestHandler):
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)
@@ -176,7 +183,8 @@ class CallbackHandler(BaseHTTPRequestHandler):
<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;">This information has been automatically saved to your .env file.</p> <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> <p style="margin-top: 40px; color: #444;">You can now close this window and restart the server.</p>
</body> </body>
@@ -199,10 +207,13 @@ STRAVA_REFRESH_TOKEN={refresh_token}</pre>
def save_to_env(client_id, client_secret, refresh_token=None): def save_to_env(client_id, client_secret, refresh_token=None):
env_path = ".env" # Always save to the platform config directory so uvx finds it
ensure_config_dir()
config_path = get_config_file()
lines = [] lines = []
if os.path.exists(env_path): if config_path.exists():
with open(env_path, "r") as f: with open(config_path, "r") as f:
lines = f.readlines() lines = f.readlines()
keys_to_set = { keys_to_set = {
@@ -230,12 +241,11 @@ def save_to_env(client_id, client_secret, refresh_token=None):
if key not in seen_keys: if key not in seen_keys:
new_lines.append(f"{key}={value}\n") new_lines.append(f"{key}={value}\n")
with open(env_path, "w") as f: with open(config_path, "w") as f:
f.writelines(new_lines) f.writelines(new_lines)
# Debug output
saved_keys = ", ".join(keys_to_set.keys()) saved_keys = ", ".join(keys_to_set.keys())
print(f"📝 Updated .env with: {saved_keys}") print(f"📝 Saved to {config_path}: {saved_keys}")
def main(): def main():
@@ -275,6 +285,7 @@ def main():
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("\n" + "=" * 60) print("\n" + "=" * 60)
print(" Strava OAuth2 Authorization") print(" Strava OAuth2 Authorization")
+6 -1
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: