feat: migrate credential storage to platform-specific configuration directories
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
STRAVA_CLIENT_ID=16037
|
||||||
|
STRAVA_CLIENT_SECRET=cc332cb8b0f7f44dac80100be87495a0a1440a2d
|
||||||
|
STRAVA_REFRESH_TOKEN=bb644951ca96e811f9520794c607a0e9b6505888
|
||||||
@@ -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,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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user