strava-activity-mcp-server 0.1.9__py3-none-any.whl → 0.2.1__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.
- strava_activity_mcp_server/__init__.py +3 -3
- strava_activity_mcp_server/strava_activity_mcp_server.py +124 -48
- {strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/METADATA +4 -3
- strava_activity_mcp_server-0.2.1.dist-info/RECORD +8 -0
- strava_activity_mcp_server-0.1.9.dist-info/RECORD +0 -8
- {strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/WHEEL +0 -0
- {strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/entry_points.txt +0 -0
- {strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
from .strava_activity_mcp_server import mcp
|
2
|
-
def main() -> None:
|
3
|
-
"""Run the MCP server."""
|
1
|
+
from .strava_activity_mcp_server import mcp
|
2
|
+
def main() -> None:
|
3
|
+
"""Run the MCP server."""
|
4
4
|
mcp.run()
|
@@ -7,7 +7,6 @@ import requests
|
|
7
7
|
import urllib.parse
|
8
8
|
|
9
9
|
@mcp.tool("strava://auth/url")
|
10
|
-
|
11
10
|
def get_auth_url(client_id: int | None = None):
|
12
11
|
"""Return the Strava OAuth authorization URL. If client_id is not provided,
|
13
12
|
read it from the STRAVA_CLIENT_ID environment variable."""
|
@@ -27,74 +26,151 @@ def get_auth_url(client_id: int | None = None):
|
|
27
26
|
"approval_prompt": "force",
|
28
27
|
"scope": "read,activity:read_all",
|
29
28
|
}
|
29
|
+
# Always return whole URL and not part of it
|
30
30
|
return "https://www.strava.com/oauth/authorize?" + urllib.parse.urlencode(params)
|
31
31
|
|
32
|
+
@mcp.tool("strava://auth/refresh")
|
33
|
+
def refresh_access_token(
|
34
|
+
refresh_token: str,
|
35
|
+
client_id: int | None = None,
|
36
|
+
client_secret: str | None = None,
|
37
|
+
) -> dict:
|
38
|
+
"""Refresh an access token using a refresh token."""
|
39
|
+
if not refresh_token:
|
40
|
+
return {"error": "refresh token is required"}
|
41
|
+
|
42
|
+
if client_id is None:
|
43
|
+
client_id_env = os.getenv("STRAVA_CLIENT_ID")
|
44
|
+
if not client_id_env:
|
45
|
+
return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
|
46
|
+
try:
|
47
|
+
client_id = int(client_id_env)
|
48
|
+
except ValueError:
|
49
|
+
return {"error": "STRAVA_CLIENT_ID must be an integer"}
|
32
50
|
|
51
|
+
if client_secret is None:
|
52
|
+
client_secret_env = os.getenv("STRAVA_CLIENT_SECRET")
|
53
|
+
if not client_secret_env:
|
54
|
+
return {"error": "STRAVA_CLIENT_SECRET environment variable is not set"}
|
55
|
+
try:
|
56
|
+
client_secret = str(client_secret_env)
|
57
|
+
except ValueError:
|
58
|
+
return {"error": "STRAVA_CLIENT_SECRET must be a string"}
|
59
|
+
|
60
|
+
resp = requests.post(
|
61
|
+
"https://www.strava.com/oauth/token",
|
62
|
+
data={
|
63
|
+
"client_id": client_id,
|
64
|
+
"client_secret": client_secret,
|
65
|
+
"refresh_token": refresh_token,
|
66
|
+
"grant_type": "refresh_token",
|
67
|
+
},
|
68
|
+
)
|
69
|
+
|
70
|
+
try:
|
71
|
+
resp.raise_for_status()
|
72
|
+
except requests.HTTPError:
|
73
|
+
return {"error": "token refresh failed", "status_code": resp.status_code, "response": resp.text}
|
74
|
+
except Exception as e:
|
75
|
+
return {"error": "token refresh failed", "status_code": resp.status_code, "response": resp.text, "error": str(e)}
|
76
|
+
|
77
|
+
tokens = resp.json()
|
78
|
+
print(tokens) # Print tokens for debugging (optional)
|
79
|
+
|
80
|
+
return {
|
81
|
+
"access_token": tokens.get("access_token"),
|
82
|
+
"refresh_token": tokens.get("refresh_token"),
|
83
|
+
"expires_at": tokens.get("expires_at"),
|
84
|
+
"expires_in": tokens.get("expires_in")
|
85
|
+
}
|
33
86
|
|
34
87
|
@mcp.tool("strava://athlete/stats")
|
35
88
|
def get_athlete_stats(
|
36
89
|
code: str,
|
37
90
|
client_id: int | None = None,
|
38
|
-
client_secret: str | None = None,
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
data={
|
91
|
+
client_secret: str | None = None,
|
92
|
+
) -> dict:
|
93
|
+
"""Exchange an authorization code for access + refresh tokens and get athlete activities."""
|
94
|
+
if not code:
|
95
|
+
return {"error": "authorization code is required"}
|
96
|
+
|
97
|
+
if client_id is None:
|
98
|
+
client_id_env = os.getenv("STRAVA_CLIENT_ID")
|
99
|
+
if not client_id_env:
|
100
|
+
return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
|
101
|
+
try:
|
102
|
+
client_id = int(client_id_env)
|
103
|
+
except ValueError:
|
104
|
+
return {"error": "STRAVA_CLIENT_ID must be an integer"}
|
105
|
+
|
106
|
+
if client_secret is None:
|
107
|
+
client_secret_env = os.getenv("STRAVA_CLIENT_SECRET")
|
108
|
+
if not client_secret_env:
|
109
|
+
return {"error": "STRAVA_CLIENT_SECRET environment variable is not set"}
|
110
|
+
try:
|
111
|
+
client_secret = str(client_secret_env)
|
112
|
+
except ValueError:
|
113
|
+
return {"error": "STRAVA_CLIENT_SECRET must be a string"}
|
114
|
+
|
115
|
+
resp = requests.post(
|
116
|
+
"https://www.strava.com/oauth/token",
|
117
|
+
data={
|
66
118
|
"client_id": client_id,
|
67
119
|
"client_secret": client_secret,
|
68
120
|
"code": code,
|
69
121
|
"grant_type": "authorization_code",
|
70
122
|
},
|
71
123
|
)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
124
|
+
|
125
|
+
try:
|
126
|
+
resp.raise_for_status()
|
127
|
+
except requests.HTTPError:
|
128
|
+
return {"error": "token request failed", "status_code": resp.status_code, "response": resp.text}
|
129
|
+
except Exception as e:
|
130
|
+
return {"error": "token request failed", "status_code": resp.status_code, "response": resp.text, "error": str(e)}
|
131
|
+
|
132
|
+
tokens = resp.json()
|
80
133
|
# Print tokens for debugging (optional)
|
81
|
-
|
134
|
+
print(tokens)
|
135
|
+
|
136
|
+
access_token = tokens.get("access_token")
|
137
|
+
refresh_token = tokens.get("refresh_token")
|
82
138
|
|
83
|
-
|
84
|
-
refresh_token = tokens.get("refresh_token")
|
139
|
+
# return {"tokens": tokens, "access_token": access_token, "refresh_token": refresh_token}
|
85
140
|
|
141
|
+
url = "https://www.strava.com/api/v3/athlete/activities?per_page=60"
|
142
|
+
headers = {
|
143
|
+
"accept": "application/json",
|
144
|
+
"authorization": f"Bearer {access_token}"
|
145
|
+
}
|
86
146
|
|
87
|
-
|
147
|
+
response = requests.get(url, headers=headers)
|
88
148
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
149
|
+
return response.json()
|
150
|
+
|
151
|
+
@mcp.tool("strava://athlete/stats-with-token")
|
152
|
+
def get_athlete_stats_with_token(access_token: str) -> dict:
|
153
|
+
"""Get athlete activities using an existing access token."""
|
154
|
+
if not access_token:
|
155
|
+
return {"error": "access token is required"}
|
156
|
+
|
157
|
+
url = "https://www.strava.com/api/v3/athlete/activities?per_page=60"
|
158
|
+
headers = {
|
159
|
+
"accept": "application/json",
|
160
|
+
"authorization": f"Bearer {access_token}"
|
161
|
+
}
|
162
|
+
|
163
|
+
response = requests.get(url, headers=headers)
|
164
|
+
|
165
|
+
try:
|
166
|
+
response.raise_for_status()
|
167
|
+
except requests.HTTPError:
|
168
|
+
return {"error": "API request failed", "status_code": response.status_code, "response": response.text}
|
169
|
+
except Exception as e:
|
170
|
+
return {"error": "API request failed", "status_code": response.status_code, "response": response.text, "error": str(e)}
|
94
171
|
|
95
|
-
|
172
|
+
return response.json()
|
96
173
|
|
97
|
-
return response.json()
|
98
174
|
|
99
175
|
if __name__ == "__main__":
|
100
176
|
mcp.run(transport="stdio") # Run the server, using standard input/output for communication
|
{strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: strava-activity-mcp-server
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: STRAVA ACTIVITY MCP SERVER
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.10
|
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
|
|
13
13
|

|
14
14
|
[](https://opensource.org/licenses/gpl-3-0)
|
15
15
|
[](https://www.python.org/downloads/release/python-3130/)
|
16
|
+
[](https://pypistats.org/packages/strava-activity-mcp-server)
|
16
17
|
|
17
18
|

|
18
19
|
|
@@ -117,10 +118,10 @@ Any MCP-capable client can launch the server using a config similar to the follo
|
|
117
118
|
To quickly test the server using the Model Context Protocol inspector tool, run:
|
118
119
|
|
119
120
|
```powershell
|
120
|
-
npx @modelcontextprotocol/inspector uvx strava-mcp-server
|
121
|
+
npx @modelcontextprotocol/inspector uvx strava-activity-mcp-server
|
121
122
|
```
|
122
123
|
|
123
|
-
This will attempt to start the server with the `uvx` transport and connect the inspector to the running MCP server instance named `strava-mcp-server`.
|
124
|
+
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`.
|
124
125
|
|
125
126
|
|
126
127
|
## Contributing
|
@@ -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=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,,
|
@@ -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=lfgnxF8w_NBl_NRwYcL2stGedwvKkMkKbwZy7TK0L74,3729
|
4
|
-
strava_activity_mcp_server-0.1.9.dist-info/METADATA,sha256=ZWdgz3TfsmOCwb1cJ3FuqlRt6xklUigz30DPoQ-rPmQ,5112
|
5
|
-
strava_activity_mcp_server-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
-
strava_activity_mcp_server-0.1.9.dist-info/entry_points.txt,sha256=F6PO_DBSThhtmX2AC-tu2MIiCJkGi31LCaQJxfUzZ5g,79
|
7
|
-
strava_activity_mcp_server-0.1.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
8
|
-
strava_activity_mcp_server-0.1.9.dist-info/RECORD,,
|
{strava_activity_mcp_server-0.1.9.dist-info → strava_activity_mcp_server-0.2.1.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|