strava-activity-mcp-server 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,16 +97,109 @@ def refresh_access_token(
97
97
 
98
98
 
99
99
  @mcp.tool("strava://athlete/stats")
100
- def get_athlete_stats(token: str) -> object:
101
- """Retrieve athlete activities using an access token."""
100
+ def _get_env_client_credentials() -> tuple[int | None, str | None]:
101
+ """Read client id and secret from environment and return (client_id, client_secret).
102
+
103
+ client_id will be returned as int if present and valid, otherwise None.
104
+ """
105
+ client_id = None
106
+ client_secret = os.getenv("STRAVA_CLIENT_SECRET")
107
+ client_id_env = os.getenv("STRAVA_CLIENT_ID")
108
+ if client_id_env:
109
+ try:
110
+ client_id = int(client_id_env)
111
+ except ValueError:
112
+ client_id = None
113
+ return client_id, client_secret
114
+
115
+
116
+ def _ensure_access_token(token_or_tokens: object) -> tuple[str | None, dict | None]:
117
+ """Given either an access token string or the token dict returned by the token endpoints,
118
+ return a tuple (access_token, tokens_dict).
119
+
120
+ If a dict is provided and contains no valid access_token but has a refresh_token,
121
+ attempt to refresh using env client credentials. Returns (access_token, tokens_dict) or (None, None)
122
+ on failure.
123
+ """
124
+ # If token_or_tokens is a string, assume it's an access token.
125
+ if isinstance(token_or_tokens, str):
126
+ return token_or_tokens, None
127
+
128
+ # If it's a dict-like object, try to find access_token
129
+ if isinstance(token_or_tokens, dict):
130
+ access_token = token_or_tokens.get("access_token")
131
+ if access_token:
132
+ return access_token, token_or_tokens
133
+
134
+ # try refresh flow
135
+ refresh_token = token_or_tokens.get("refresh_token")
136
+ client_id = token_or_tokens.get("client_id")
137
+ client_secret = token_or_tokens.get("client_secret")
138
+
139
+ # fallback to env vars if client id/secret not in the dict
140
+ if not client_id or not client_secret:
141
+ env_client_id, env_client_secret = _get_env_client_credentials()
142
+ if not client_id:
143
+ client_id = env_client_id
144
+ if not client_secret:
145
+ client_secret = env_client_secret
146
+
147
+ if refresh_token and client_id and client_secret:
148
+ try:
149
+ new_tokens = refresh_access_token(refresh_token, int(client_id), client_secret)
150
+ except Exception as e:
151
+ print(f"refresh failed: {e}")
152
+ return None, None
153
+
154
+ access_token = new_tokens.get("access_token")
155
+ return access_token, new_tokens
156
+
157
+ return None, None
158
+
159
+
160
+ @mcp.tool("strava://athlete/stats")
161
+ def get_athlete_stats(token: object) -> object:
162
+ """Retrieve athlete activities using either an access token string or a token dict.
163
+
164
+ If a token dict is provided and the access token is missing/expired, the function will
165
+ attempt to refresh it (one attempt) using provided refresh token and client credentials
166
+ (falling back to STRAVA_CLIENT_ID/STRAVA_CLIENT_SECRET environment variables).
167
+ """
168
+ access_token, tokens_dict = _ensure_access_token(token)
169
+ if not access_token:
170
+ return {"error": "Could not obtain an access token"}
171
+
102
172
  url = "https://www.strava.com/api/v3/athlete/activities?per_page=60"
103
173
  headers = {
104
174
  "accept": "application/json",
105
- "authorization": f"Bearer {token}"
175
+ "authorization": f"Bearer {access_token}"
106
176
  }
177
+
107
178
  response = requests.get(url, headers=headers)
108
- response.raise_for_status()
109
- # Return the parsed JSON (dict or list) instead of a JSON string so the return type matches.
179
+
180
+ # If unauthorized, try one refresh if we have a refresh token available
181
+ if response.status_code == 401 and isinstance(token, dict):
182
+ refresh_token = token.get("refresh_token") or (tokens_dict or {}).get("refresh_token")
183
+ if refresh_token:
184
+ client_id = token.get("client_id") or (tokens_dict or {}).get("client_id")
185
+ client_secret = token.get("client_secret") or (tokens_dict or {}).get("client_secret")
186
+ if not client_id or not client_secret:
187
+ env_client_id, env_client_secret = _get_env_client_credentials()
188
+ client_id = client_id or env_client_id
189
+ client_secret = client_secret or env_client_secret
190
+
191
+ if client_id and client_secret:
192
+ new_tokens = refresh_access_token(refresh_token, int(client_id), client_secret)
193
+ new_access = new_tokens.get("access_token")
194
+ if new_access:
195
+ headers["authorization"] = f"Bearer {new_access}"
196
+ response = requests.get(url, headers=headers)
197
+
198
+ try:
199
+ response.raise_for_status()
200
+ except requests.HTTPError:
201
+ return {"error": "request failed", "status_code": response.status_code, "response": response.text}
202
+
110
203
  return response.json()
111
204
 
112
205
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strava-activity-mcp-server
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Trying to implement environment variables for client_id
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -0,0 +1,8 @@
1
+ strava_activity_mcp_server/__init__.py,sha256=NgXC8CeBg0qFRHdZJJKjQlX9_RwSETJ9O6PNy0leOTI,107
2
+ strava_activity_mcp_server/__main__.py,sha256=SAdVoObdjb5UP4MY-Y2_uCXpnthB6hgxlb1KNVNgOrc,58
3
+ strava_activity_mcp_server/strava_activity_mcp_server.py,sha256=3SSoZ831XiPZqQM5l-Bm_75NYJesy22Iq13gor5reO8,7879
4
+ strava_activity_mcp_server-0.1.4.dist-info/METADATA,sha256=0LmTHJSvsxAcPLRpwJf1cW2k8FZicPx62LoMirtSQTU,6043
5
+ strava_activity_mcp_server-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ strava_activity_mcp_server-0.1.4.dist-info/entry_points.txt,sha256=F6PO_DBSThhtmX2AC-tu2MIiCJkGi31LCaQJxfUzZ5g,79
7
+ strava_activity_mcp_server-0.1.4.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
8
+ strava_activity_mcp_server-0.1.4.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- strava_activity_mcp_server/__init__.py,sha256=NgXC8CeBg0qFRHdZJJKjQlX9_RwSETJ9O6PNy0leOTI,107
2
- strava_activity_mcp_server/__main__.py,sha256=SAdVoObdjb5UP4MY-Y2_uCXpnthB6hgxlb1KNVNgOrc,58
3
- strava_activity_mcp_server/strava_activity_mcp_server.py,sha256=rZgRFH4esSiOY-ouceIR046hzTpOeN6xDE_eRZPY07U,3836
4
- strava_activity_mcp_server-0.1.3.dist-info/METADATA,sha256=uavNysuFJgKieRxztWtYi6C6Lc7MidtCp3BG9odCAh0,6043
5
- strava_activity_mcp_server-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- strava_activity_mcp_server-0.1.3.dist-info/entry_points.txt,sha256=F6PO_DBSThhtmX2AC-tu2MIiCJkGi31LCaQJxfUzZ5g,79
7
- strava_activity_mcp_server-0.1.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
8
- strava_activity_mcp_server-0.1.3.dist-info/RECORD,,