strava-activity-mcp-server 0.2.1__py3-none-any.whl → 0.2.3__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.

Potentially problematic release.


This version of strava-activity-mcp-server might be problematic. Click here for more details.

@@ -5,9 +5,37 @@ mcp = FastMCP("Strava") # Initialize an MCP server instance with a descriptive
5
5
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
6
  import requests
7
7
  import urllib.parse
8
+ import json
9
+ from typing import Any, Dict
10
+
11
+ TOKEN_STORE_FILENAME = "strava_mcp_tokens.json"
12
+
13
+ def _get_token_store_path() -> str:
14
+ home_dir = os.path.expanduser("~")
15
+ return os.path.join(home_dir, TOKEN_STORE_FILENAME)
16
+
17
+ def _save_tokens_to_disk(tokens: Dict[str, Any]) -> dict:
18
+ try:
19
+ path = _get_token_store_path()
20
+ with open(path, "w", encoding="utf-8") as f:
21
+ json.dump(tokens, f)
22
+ return {"ok": True, "path": path}
23
+ except Exception as e:
24
+ return {"ok": False, "error": str(e)}
25
+
26
+ def _load_tokens_from_disk() -> dict:
27
+ try:
28
+ path = _get_token_store_path()
29
+ if not os.path.exists(path):
30
+ return {"ok": False, "error": "token store not found", "path": path}
31
+ with open(path, "r", encoding="utf-8") as f:
32
+ data = json.load(f)
33
+ return {"ok": True, "tokens": data, "path": path}
34
+ except Exception as e:
35
+ return {"ok": False, "error": str(e)}
8
36
 
9
37
  @mcp.tool("strava://auth/url")
10
- def get_auth_url(client_id: int | None = None):
38
+ async def get_auth_url(client_id: int | None = None):
11
39
  """Return the Strava OAuth authorization URL. If client_id is not provided,
12
40
  read it from the STRAVA_CLIENT_ID environment variable."""
13
41
  if client_id is None:
@@ -30,7 +58,8 @@ def get_auth_url(client_id: int | None = None):
30
58
  return "https://www.strava.com/oauth/authorize?" + urllib.parse.urlencode(params)
31
59
 
32
60
  @mcp.tool("strava://auth/refresh")
33
- def refresh_access_token(
61
+ async def refresh_access_token(
62
+
34
63
  refresh_token: str,
35
64
  client_id: int | None = None,
36
65
  client_secret: str | None = None,
@@ -85,7 +114,7 @@ def refresh_access_token(
85
114
  }
86
115
 
87
116
  @mcp.tool("strava://athlete/stats")
88
- def get_athlete_stats(
117
+ async def get_athlete_stats(
89
118
  code: str,
90
119
  client_id: int | None = None,
91
120
  client_secret: str | None = None,
@@ -135,6 +164,17 @@ def get_athlete_stats(
135
164
 
136
165
  access_token = tokens.get("access_token")
137
166
  refresh_token = tokens.get("refresh_token")
167
+
168
+ # Persist tokens for later refresh usage via the public save tool
169
+ save_result = await save_tokens({
170
+ "access_token": access_token,
171
+ "refresh_token": refresh_token,
172
+ "expires_at": tokens.get("expires_at"),
173
+ "expires_in": tokens.get("expires_in"),
174
+ "athlete": tokens.get("athlete"),
175
+ "token_type": tokens.get("token_type"),
176
+ "scope": tokens.get("scope"),
177
+ })
138
178
 
139
179
  # return {"tokens": tokens, "access_token": access_token, "refresh_token": refresh_token}
140
180
 
@@ -145,11 +185,21 @@ def get_athlete_stats(
145
185
  }
146
186
 
147
187
  response = requests.get(url, headers=headers)
188
+ return {
189
+ "activities": response.json(),
190
+ "tokens": {
191
+ "access_token": access_token,
192
+ "refresh_token": refresh_token,
193
+ "expires_at": tokens.get("expires_at"),
194
+ "expires_in": tokens.get("expires_in"),
195
+ },
196
+ "save": save_result
197
+ }
148
198
 
149
199
  return response.json()
150
200
 
151
201
  @mcp.tool("strava://athlete/stats-with-token")
152
- def get_athlete_stats_with_token(access_token: str) -> dict:
202
+ async def get_athlete_stats_with_token(access_token: str) -> dict:
153
203
  """Get athlete activities using an existing access token."""
154
204
  if not access_token:
155
205
  return {"error": "access token is required"}
@@ -171,6 +221,75 @@ def get_athlete_stats_with_token(access_token: str) -> dict:
171
221
 
172
222
  return response.json()
173
223
 
224
+ @mcp.tool("strava://auth/save")
225
+ async def save_tokens(tokens: dict | None = None) -> dict:
226
+ """Save tokens to local disk at ~/.strava_mcp_tokens.json. If tokens is not provided, no-op with error."""
227
+ if not tokens or not isinstance(tokens, dict):
228
+ return {"error": "tokens dict is required"}
229
+ result = _save_tokens_to_disk(tokens)
230
+ if not result.get("ok"):
231
+ return {"error": "failed to save tokens", **result}
232
+ return {"ok": True, "path": result.get("path")}
233
+
234
+
235
+ @mcp.tool("strava://auth/load")
236
+ async def load_tokens() -> dict:
237
+ """Load tokens from local disk at ~/.strava_mcp_tokens.json"""
238
+ result = _load_tokens_from_disk()
239
+ if not result.get("ok"):
240
+ return {"error": result.get("error"), "path": result.get("path")}
241
+ return {"ok": True, "tokens": result.get("tokens"), "path": result.get("path")}
242
+
243
+ @mcp.tool("strava://athlete/refresh-and-stats")
244
+ async def refresh_and_get_stats(client_id: int | None = None, client_secret: str | None = None) -> dict:
245
+ """Load saved refresh token, refresh access token, save it, then fetch activities."""
246
+ load_result = await load_tokens()
247
+ if not load_result.get("ok"):
248
+ return {"error": "no saved tokens", "details": load_result}
249
+ saved = load_result.get("tokens", {})
250
+ refresh_token = saved.get("refresh_token")
251
+ if not refresh_token:
252
+ return {"error": "refresh_token not found in saved tokens"}
253
+
254
+ refreshed = await refresh_access_token(refresh_token=refresh_token, client_id=client_id, client_secret=client_secret)
255
+ if "error" in refreshed:
256
+ return {"error": "refresh failed", "details": refreshed}
257
+
258
+ # Save refreshed tokens
259
+ await save_tokens(refreshed)
260
+
261
+ access_token = refreshed.get("access_token")
262
+ if not access_token:
263
+ return {"error": "no access_token after refresh"}
264
+
265
+ # Fetch activities with new token
266
+ activities = await get_athlete_stats_with_token(access_token)
267
+ return {"activities": activities, "tokens": refreshed}
268
+
269
+ @mcp.tool("strava://session/start")
270
+ async def start_session(client_id: int | None = None, client_secret: str | None = None) -> dict:
271
+ """Start a session: if a refresh token exists, refresh and fetch; otherwise return auth URL."""
272
+ token_path = _get_token_store_path()
273
+ if os.path.exists(token_path):
274
+ loaded = _load_tokens_from_disk()
275
+ if loaded.get("ok"):
276
+ saved = loaded.get("tokens", {})
277
+ refresh_token = saved.get("refresh_token")
278
+ if isinstance(refresh_token, str) and refresh_token.strip():
279
+ result = await refresh_and_get_stats(client_id=client_id, client_secret=client_secret)
280
+ return {**result, "used_token_file": token_path}
281
+ # Fall back to auth URL flow
282
+ else:
283
+ url = await get_auth_url(client_id=client_id)
284
+ return {"auth_url": url, "token_file_checked": token_path}
285
+
286
+ #@mcp.prompt
287
+ #def greet_user_prompt(question: str) -> str:
288
+ #"""Generates a message orchestrating mcp tools"""
289
+ #return f"""
290
+ #Return a message for a user called '{question}'.
291
+ #if the user is asking, use a formal style, else use a street style.
292
+ #"""
174
293
 
175
294
  if __name__ == "__main__":
176
295
  mcp.run(transport="stdio") # Run the server, using standard input/output for communication
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strava-activity-mcp-server
3
- Version: 0.2.1
4
- Summary: STRAVA ACTIVITY MCP SERVER
3
+ Version: 0.2.3
4
+ Summary: Strava MCP server: one-time browser auth, then automatic refresh-token login
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
7
7
  Requires-Dist: build>=1.3.0
@@ -21,6 +21,8 @@ Description-Content-Type: text/markdown
21
21
 
22
22
  A small Model Context Protocol (MCP) server that exposes your Strava athlete data to language-model tooling.
23
23
 
24
+ After the first browser-based authorization, the server uses the saved `refresh_token` to automatically refresh your session; no further URL-redirected logins are required on subsequent runs.
25
+
24
26
  This package provides a lightweight MCP server which communicates with the Strava API and exposes a few helper tools (authorization URL, token exchange/refresh, and fetching athlete activities) that language models or other local tools can call.
25
27
 
26
28
  The project is intended to be used locally (for example with Claude MCP integrations) and is published on PyPI as `strava-activity-mcp-server`.
@@ -55,7 +57,7 @@ from strava_activity_mcp_server import main
55
57
  main()
56
58
  ```
57
59
 
58
- By default the server starts the MCP runtime; when used with an MCP-aware client (for example Claude MCP integrations) the exposed tools become callable.
60
+ By default the server starts the MCP runtime; when used with an MCP-aware client (for example Msty MCP orsome other MCP integrations such Claude, LM Tool and etc.) the exposed tools become callable.
59
61
 
60
62
  ## Authentication (Strava OAuth)
61
63
 
@@ -63,28 +65,54 @@ This server requires Strava OAuth credentials to access athlete data. You will n
63
65
 
64
66
  - STRAVA_CLIENT_ID
65
67
  - STRAVA_CLIENT_SECRET
66
- - STRAVA_REFRESH_TOKEN (or a short-lived access token obtained from the authorization flow)
67
68
 
68
69
  Steps:
69
70
 
70
71
  1. Create a Strava API application at https://www.strava.com/settings/api and note your Client ID and Client Secret. Use `localhost` as the Authorization Callback Domain.
71
- 2. Open the authorization URL produced by the `strava://auth/url` tool (see IMAGE below) in a browser to obtain an authorization code.
72
+ 2. Initial authorization: call the `strava://auth/url` tool to generate an authorization URL (see IMAGE below), open it in your browser, and grant access. This step is only needed the first time to obtain an authorization code.
72
73
 
73
74
  ![auth](https://github.com/user-attachments/assets/a348ccc7-a4be-49fb-8f79-b88f9d80cfc9)
74
75
 
75
- 3. Copy the code from the redirected URL (Image below) or use the included helper to save refresh/access tokens to your environment.
76
+ 3. Copy the `code` from the redirected URL (Image below). Use the provided tools to exchange it for access/refresh tokens.
76
77
 
77
78
  ![code](https://github.com/user-attachments/assets/0bb54edb-c9f9-4416-8fb2-c7e0a38d11c9)
78
79
 
79
80
 
80
- ## Exposed Tools (what the server provides)
81
+ 4. After the initial authorization, a token file named `strava_mcp_tokens.json` is created and stored in your home directory (for example on Windows: `C:\\Users\\<YourUserName>\\strava_mcp_tokens.json`). This file contains your `refresh_token`, which will be used automatically for subsequent logins. After the first authorization you do not need to open the browser flow again; future runs refresh the access token from the locally stored `refresh_token`.
82
+
83
+
81
84
 
82
- The MCP server exposes the following tools (tool IDs shown):
83
85
 
84
- - `strava://auth/url` Build the Strava OAuth authorization URL. Input: `client_id` (int). Output: URL string to open in a browser.
85
- - `strava://athlete/stats` — Fetch recent athlete activities. Input: `client_id` (int), `client_secret` (str), `access_token` (str) and obtained `code` from URL generated by `strava://auth/url`. Output: JSON with activity list.
86
+ ## Exposed Tools (what the server provides)
86
87
 
87
- These tools map to the functions implemented in `src/strava_activity_mcp_server/strava_activity_mcp_server.py` and are intended to be called by MCP clients.
88
+ The MCP server exposes the following tools (tool IDs shown). These map to functions in `src/strava_activity_mcp_server/strava_activity_mcp_server.py` and cover both initial authorization and subsequent refresh flows:
89
+
90
+ - `strava://auth/url` — Build the Strava OAuth authorization URL.
91
+ - Inputs: `client_id` (int, optional; reads `STRAVA_CLIENT_ID` if omitted)
92
+ - Output: Authorization URL string
93
+ - `strava://auth/refresh` — Refresh an access token using a refresh token.
94
+ - Inputs: `refresh_token` (str), `client_id` (int, optional), `client_secret` (str, optional)
95
+ - Output: `{ access_token, refresh_token, expires_at, expires_in }`
96
+ - `strava://athlete/stats` — Exchange an authorization `code` for tokens and then fetch recent activities.
97
+ - Inputs: `code` (str), `client_id` (int, optional), `client_secret` (str, optional)
98
+ - Output: `{ activities, tokens, save }`
99
+ - `strava://athlete/stats-with-token` — Fetch recent activities using an existing access token.
100
+ - Inputs: `access_token` (str)
101
+ - Output: Activity list (JSON)
102
+ - `strava://auth/save` — Save tokens to `~\strava_mcp_tokens.json`.
103
+ - Inputs: `tokens` (dict)
104
+ - Output: `{ ok, path }` or error
105
+ - `strava://auth/load` — Load tokens from `~\strava_mcp_tokens.json`.
106
+ - Inputs: none
107
+ - Output: `{ ok, tokens, path }` or error
108
+ - `strava://athlete/refresh-and-stats` — Load saved refresh token, refresh access token, save it, and fetch activities.
109
+ - Inputs: `client_id` (int, optional), `client_secret` (str, optional)
110
+ - Output: `{ activities, tokens }`
111
+ - `strava://session/start` — Convenience entry: if tokens exist, refresh and fetch; otherwise return an auth URL to begin initial authorization.
112
+ - Inputs: `client_id` (int, optional), `client_secret` (str, optional)
113
+ - Output: Either `{ activities, tokens }` or `{ auth_url, token_file_checked }`
114
+
115
+ These tools are intended to be called by MCP clients.
88
116
 
89
117
  ## Example flows
90
118
 
@@ -109,8 +137,7 @@ Any MCP-capable client can launch the server using a config similar to the follo
109
137
  ],
110
138
  "env": {
111
139
  "STRAVA_CLIENT_ID": "12345",
112
- "STRAVA_CLIENT_SECRET": "e1234a12d12345f12c1f12345a123bba1d12c1",
113
- "STRAVA_REFRESH_TOKEN": "1a123eda1cfd12345678987db2db1bda234c38"
140
+ "STRAVA_CLIENT_SECRET": "e1234a12d12345f12c1f12345a123bba1d12c1"
114
141
  }
115
142
  }
116
143
  ```
@@ -123,6 +150,13 @@ npx @modelcontextprotocol/inspector uvx strava-activity-mcp-server
123
150
 
124
151
  This will attempt to start the server with the `uvx` transport and connect the inspector to the running MCP server instance named `strava-activity-mcp-server`.
125
152
 
153
+ ## Chat example using MCP in Msty Studio
154
+
155
+ ![chat_1](https://github.com/user-attachments/assets/460cced5-15b3-41eb-9805-72966826ede8)
156
+ ![chat_2](https://github.com/user-attachments/assets/9ded03f3-0f86-400e-8ebc-c414d0346257)
157
+ ![chat_3](https://github.com/user-attachments/assets/d793c9a5-8fb2-430e-a0bf-679903cf3f97)
158
+ ![chat_4](https://github.com/user-attachments/assets/4a459c31-3b42-4c32-8685-e6dd851dadca)
159
+
126
160
 
127
161
  ## Contributing
128
162
 
@@ -140,3 +174,4 @@ This project is licensed under the GNU GENERAL PUBLIC LICENSE — see the `LICEN
140
174
 
141
175
 
142
176
 
177
+
@@ -0,0 +1,8 @@
1
+ strava_activity_mcp_server/__init__.py,sha256=jaa1ZVuEJMwuVGzj67oqC_ESUUiwblVVH-NEtTiQQdQ,110
2
+ strava_activity_mcp_server/__main__.py,sha256=SAdVoObdjb5UP4MY-Y2_uCXpnthB6hgxlb1KNVNgOrc,58
3
+ strava_activity_mcp_server/strava_activity_mcp_server.py,sha256=hdk_t8IveOFvfm5K2irlzXb8GIHikvmFKIZQIqRn_OE,11488
4
+ strava_activity_mcp_server-0.2.3.dist-info/METADATA,sha256=cXIW5XIKPUzay262mC2uGFfjIPSj21FS0gh1FRITLzY,7665
5
+ strava_activity_mcp_server-0.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
+ strava_activity_mcp_server-0.2.3.dist-info/entry_points.txt,sha256=F6PO_DBSThhtmX2AC-tu2MIiCJkGi31LCaQJxfUzZ5g,79
7
+ strava_activity_mcp_server-0.2.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
8
+ strava_activity_mcp_server-0.2.3.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- strava_activity_mcp_server/__init__.py,sha256=jaa1ZVuEJMwuVGzj67oqC_ESUUiwblVVH-NEtTiQQdQ,110
2
- strava_activity_mcp_server/__main__.py,sha256=SAdVoObdjb5UP4MY-Y2_uCXpnthB6hgxlb1KNVNgOrc,58
3
- strava_activity_mcp_server/strava_activity_mcp_server.py,sha256=3qxN4c8cLlVlv4R1e90c8AOsw6WaCOy4PMz2GwXsCwE,6515
4
- strava_activity_mcp_server-0.2.1.dist-info/METADATA,sha256=5XqxiNyEzQmiF26Fh_NrDfTtGJMZSu-e4MSstL9-dNM,5270
5
- strava_activity_mcp_server-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
6
- strava_activity_mcp_server-0.2.1.dist-info/entry_points.txt,sha256=F6PO_DBSThhtmX2AC-tu2MIiCJkGi31LCaQJxfUzZ5g,79
7
- strava_activity_mcp_server-0.2.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
8
- strava_activity_mcp_server-0.2.1.dist-info/RECORD,,