From b8bce4ee7fe766cad1683829142552745ddf7566 Mon Sep 17 00:00:00 2001 From: Matthias Hinrichs Date: Thu, 14 May 2026 21:29:18 +0200 Subject: [PATCH] feat: migrate credential storage to platform-specific configuration directories --- bkp-env | 3 +++ src/strava_mcp_server/config.py | 38 ++++++++++++++++++++++++++++++ src/strava_mcp_server/get_token.py | 27 ++++++++++++++------- src/strava_mcp_server/main.py | 7 +++++- 4 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 bkp-env create mode 100644 src/strava_mcp_server/config.py diff --git a/bkp-env b/bkp-env new file mode 100644 index 0000000..7c6ad11 --- /dev/null +++ b/bkp-env @@ -0,0 +1,3 @@ +STRAVA_CLIENT_ID=16037 +STRAVA_CLIENT_SECRET=cc332cb8b0f7f44dac80100be87495a0a1440a2d +STRAVA_REFRESH_TOKEN=bb644951ca96e811f9520794c607a0e9b6505888 diff --git a/src/strava_mcp_server/config.py b/src/strava_mcp_server/config.py new file mode 100644 index 0000000..898ef37 --- /dev/null +++ b/src/strava_mcp_server/config.py @@ -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\\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 diff --git a/src/strava_mcp_server/get_token.py b/src/strava_mcp_server/get_token.py index 0c42325..98e1457 100644 --- a/src/strava_mcp_server/get_token.py +++ b/src/strava_mcp_server/get_token.py @@ -19,7 +19,13 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs 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_SECRET = os.getenv("STRAVA_CLIENT_SECRET") @@ -127,6 +133,7 @@ class CallbackHandler(BaseHTTPRequestHandler): client_secret: str = "" tokens: dict = {} error: str | None = None + config_path: str = "" def do_GET(self): parsed = urlparse(self.path) @@ -176,7 +183,8 @@ class CallbackHandler(BaseHTTPRequestHandler):
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.

+

Automatically saved to:

+ {CallbackHandler.config_path}

You can now close this window and restart the server.

@@ -199,10 +207,13 @@ STRAVA_REFRESH_TOKEN={refresh_token} 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 = [] - if os.path.exists(env_path): - with open(env_path, "r") as f: + if config_path.exists(): + with open(config_path, "r") as f: lines = f.readlines() keys_to_set = { @@ -230,12 +241,11 @@ def save_to_env(client_id, client_secret, refresh_token=None): if key not in seen_keys: 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) - # Debug output saved_keys = ", ".join(keys_to_set.keys()) - print(f"📝 Updated .env with: {saved_keys}") + print(f"📝 Saved to {config_path}: {saved_keys}") def main(): @@ -275,6 +285,7 @@ def main(): CallbackHandler.client_secret = CLIENT_SECRET CallbackHandler.tokens = {} CallbackHandler.error = None + CallbackHandler.config_path = str(get_config_file()) print("\n" + "=" * 60) print(" Strava OAuth2 Authorization") diff --git a/src/strava_mcp_server/main.py b/src/strava_mcp_server/main.py index 4646540..3c3666a 100644 --- a/src/strava_mcp_server/main.py +++ b/src/strava_mcp_server/main.py @@ -5,10 +5,15 @@ import sys from dotenv import load_dotenv 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.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: