77from garminconnect import Garmin , GarminConnectConnectionError
88
99
10+ def resolve_token_path (path : str ) -> str :
11+ """Resolve environment variables and the user-home marker in a token path.
12+
13+ Some MCP clients leave ``${HOME}`` unresolved when it comes from a nested
14+ user-config default. The explicit replacement also covers Windows, where
15+ ``HOME`` may be unset but Python can still resolve ``~`` via ``USERPROFILE``.
16+ """
17+ expanded = os .path .expandvars (path )
18+ expanded = expanded .replace ("${HOME}" , os .path .expanduser ("~" ))
19+ return os .path .expanduser (expanded )
20+
21+
1022def secure_token_dir (path : str ) -> None :
1123 """Set owner-only permissions on a token directory and the files inside it.
1224
1325 OAuth tokens are ~6-month bearer credentials to the full Garmin account, so
1426 they must not be left world-readable on multi-user hosts. Safe to call on a
1527 path that is a single file rather than a directory.
1628 """
17- expanded = os . path . expanduser (path )
29+ expanded = resolve_token_path (path )
1830 if not os .path .exists (expanded ):
1931 return
2032 if os .path .isdir (expanded ):
@@ -32,7 +44,7 @@ def get_token_path() -> str:
3244 Returns:
3345 str: Path to token storage directory
3446 """
35- return os .getenv ("GARMINTOKENS" ) or "~/.garminconnect"
47+ return resolve_token_path ( os .getenv ("GARMINTOKENS" ) or "~/.garminconnect" )
3648
3749
3850def get_token_base64_path () -> str :
@@ -41,7 +53,9 @@ def get_token_base64_path() -> str:
4153 Returns:
4254 str: Path to base64 token file
4355 """
44- return os .getenv ("GARMINTOKENS_BASE64" ) or "~/.garminconnect_base64"
56+ return resolve_token_path (
57+ os .getenv ("GARMINTOKENS_BASE64" ) or "~/.garminconnect_base64"
58+ )
4559
4660
4761def token_exists (token_path : str = None ) -> bool :
@@ -56,7 +70,7 @@ def token_exists(token_path: str = None) -> bool:
5670 if token_path is None :
5771 token_path = get_token_path ()
5872
59- expanded_path = Path (os . path . expanduser (token_path ))
73+ expanded_path = Path (resolve_token_path (token_path ))
6074 return expanded_path .exists ()
6175
6276
@@ -75,6 +89,7 @@ def validate_tokens(token_path: str = None, is_cn: bool = False) -> Tuple[bool,
7589
7690 if token_path is None :
7791 token_path = get_token_path ()
92+ token_path = resolve_token_path (token_path )
7893
7994 # Check if tokens exist
8095 if not token_exists (token_path ):
@@ -138,17 +153,19 @@ def remove_tokens(token_path: str = None, base64_path: str = None) -> None:
138153 token_path = get_token_path ()
139154 if base64_path is None :
140155 base64_path = get_token_base64_path ()
156+ token_path = resolve_token_path (token_path )
157+ base64_path = resolve_token_path (base64_path )
141158
142159 # Remove token directory
143- expanded_token_path = Path (os . path . expanduser ( token_path ) )
160+ expanded_token_path = Path (token_path )
144161 if expanded_token_path .exists ():
145162 if expanded_token_path .is_dir ():
146163 shutil .rmtree (expanded_token_path )
147164 else :
148165 expanded_token_path .unlink ()
149166
150167 # Remove base64 token file
151- expanded_base64_path = Path (os . path . expanduser ( base64_path ) )
168+ expanded_base64_path = Path (base64_path )
152169 if expanded_base64_path .exists ():
153170 expanded_base64_path .unlink ()
154171
@@ -165,6 +182,7 @@ def get_token_info(token_path: str = None, is_cn: bool = False) -> dict:
165182 """
166183 if token_path is None :
167184 token_path = get_token_path ()
185+ token_path = resolve_token_path (token_path )
168186
169187 exists = token_exists (token_path )
170188 is_valid = False
@@ -175,7 +193,7 @@ def get_token_info(token_path: str = None, is_cn: bool = False) -> dict:
175193
176194 return {
177195 "path" : token_path ,
178- "expanded_path" : os . path . expanduser ( token_path ) ,
196+ "expanded_path" : token_path ,
179197 "exists" : exists ,
180198 "valid" : is_valid ,
181199 "error" : error_msg
0 commit comments