strava-activity-mcp-server 0.1.7__tar.gz → 0.1.9__tar.gz

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.
Files changed (18) hide show
  1. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/.python-version +1 -1
  2. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/PKG-INFO +25 -27
  3. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/README.md +23 -25
  4. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/pyproject.toml +18 -18
  5. strava_activity_mcp_server-0.1.9/ref/auth.jpg +0 -0
  6. strava_activity_mcp_server-0.1.9/ref/code.jpg +0 -0
  7. strava_activity_mcp_server-0.1.9/ref/image.jpg +0 -0
  8. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/src/strava_activity_mcp_server/__init__.py +3 -3
  9. strava_activity_mcp_server-0.1.9/src/strava_activity_mcp_server/strava_activity_mcp_server.py +100 -0
  10. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/uv.lock +2 -2
  11. strava_activity_mcp_server-0.1.7/src/strava_activity_mcp_server/strava_activity_mcp_server.py +0 -222
  12. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/.github/workflows/python-publish.yml +0 -0
  13. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/.gitignore +0 -0
  14. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/.vscode/settings.json +0 -0
  15. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/LICENSE +0 -0
  16. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/ref/mcp_pypi_example.md +0 -0
  17. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/requirements.txt +0 -0
  18. {strava_activity_mcp_server-0.1.7 → strava_activity_mcp_server-0.1.9}/src/strava_activity_mcp_server/__main__.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strava-activity-mcp-server
3
- Version: 0.1.7
4
- Summary: Trying to implement environment variables for client_id
3
+ Version: 0.1.9
4
+ Summary: STRAVA ACTIVITY MCP SERVER
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
7
7
  Requires-Dist: build>=1.3.0
@@ -14,6 +14,10 @@ Description-Content-Type: text/markdown
14
14
  [![License: GNU](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://opensource.org/licenses/gpl-3-0)
15
15
  [![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/downloads/release/python-3130/)
16
16
 
17
+ ![image](https://github.com/user-attachments/assets/4bb214ca-1132-4e63-9390-d6eaddab50be)
18
+
19
+
20
+
17
21
  A small Model Context Protocol (MCP) server that exposes your Strava athlete data to language-model tooling.
18
22
 
19
23
  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.
@@ -39,7 +43,7 @@ After installing, you can run the MCP server using the provided console script o
39
43
 
40
44
  Run via the console script (entry point defined in `pyproject.toml`):
41
45
 
42
- ```powershell
46
+ ```cmd
43
47
  strava-activity-mcp-server
44
48
  ```
45
49
 
@@ -63,27 +67,21 @@ This server requires Strava OAuth credentials to access athlete data. You will n
63
67
  Steps:
64
68
 
65
69
  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.
66
- 2. Open the authorization URL produced by the `strava://auth/url` tool (see Tools below) in a browser to obtain an authorization code.
67
- 3. Exchange the code for tokens using `strava://auth/token` or use the included helper to save refresh/access tokens to your environment.
70
+ 2. Open the authorization URL produced by the `strava://auth/url` tool (see IMAGE below) in a browser to obtain an authorization code.
68
71
 
69
- For local testing you can also manually set the environment variables before running the server:
72
+ ![auth](https://github.com/user-attachments/assets/a348ccc7-a4be-49fb-8f79-b88f9d80cfc9)
70
73
 
71
- ```powershell
72
- $env:STRAVA_CLIENT_ID = "<your client id>";
73
- $env:STRAVA_CLIENT_SECRET = "<your client secret>";
74
- $env:STRAVA_REFRESH_TOKEN = "<your refresh token>";
75
- strava-activity-mcp-server
76
- ```
74
+ 3. Copy the code from the redirected URL (Image below) or use the included helper to save refresh/access tokens to your environment.
75
+
76
+ ![code](https://github.com/user-attachments/assets/0bb54edb-c9f9-4416-8fb2-c7e0a38d11c9)
77
77
 
78
- Note: Keep secrets out of version control. Use a `.env` file and a tool such as `direnv` or your system secrets manager for convenience.
79
78
 
80
79
  ## Exposed Tools (what the server provides)
81
80
 
82
81
  The MCP server exposes the following tools (tool IDs shown):
83
82
 
84
83
  - `strava://auth/url` — Build the Strava OAuth authorization URL. Input: `client_id` (int). Output: URL string to open in a browser.
85
- - `strava://auth/token` — Exchange an authorization code for access + refresh tokens. Inputs: `code` (str), `client_id` (int), `client_secret` (str). Output: token dict (with `access_token`, `refresh_token`).
86
- - `strava://athlete/stats` — Fetch recent athlete activities. Input: `token` (str). Output: JSON with activity list.
84
+ - `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.
87
85
 
88
86
  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.
89
87
 
@@ -92,28 +90,27 @@ These tools map to the functions implemented in `src/strava_activity_mcp_server/
92
90
  1) Get an authorization URL and retrieve tokens
93
91
 
94
92
  - Call `strava://auth/url` with your `client_id` and open the returned URL in your browser.
95
- - After authorizing, Strava will provide a `code`. Call `strava://auth/token` with `code`, `client_id`, and `client_secret` to receive `access_token` and `refresh_token`.
93
+ - After authorizing, Strava will provide a `code`.
96
94
 
97
95
  2) Fetch recent activities
98
96
 
99
97
  - Use `strava://athlete/stats` with a valid access token. If the access token is expired, use the refresh flow to get a new access token.
100
98
 
101
- ## Developer notes
102
-
103
- - The package entry point calls `mcp.run()` which runs the MCP server. If you want to change transport or logging settings, modify `src/strava_activity_mcp_server/__init__.py` or `strava_activity_mcp_server.py`.
104
- - The code uses the `requests` library for HTTP calls.
105
-
106
-
107
99
  ### Client config example and quick inspector test
108
100
 
109
- Any MCP-capable client can launch the server using a config similar to the following (example file often called `config.json`):
101
+ Any MCP-capable client can launch the server using a config similar to the following (example file often called `config.json`. Be sure to enter your values here):
110
102
 
111
103
  ```json
112
104
  {
113
- "command": "uvx",
114
- "args": [
115
- "strava-activity-mcp-server"
116
- ]
105
+ "command": "uvx",
106
+ "args": [
107
+ "strava-activity-mcp-server"
108
+ ],
109
+ "env": {
110
+ "STRAVA_CLIENT_ID": "12345",
111
+ "STRAVA_CLIENT_SECRET": "e1234a12d12345f12c1f12345a123bba1d12c1",
112
+ "STRAVA_REFRESH_TOKEN": "1a123eda1cfd12345678987db2db1bda234c38"
113
+ }
117
114
  }
118
115
  ```
119
116
 
@@ -141,3 +138,4 @@ This project is licensed under the GNU GENERAL PUBLIC LICENSE — see the `LICEN
141
138
 
142
139
 
143
140
 
141
+
@@ -3,6 +3,10 @@
3
3
  [![License: GNU](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://opensource.org/licenses/gpl-3-0)
4
4
  [![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/downloads/release/python-3130/)
5
5
 
6
+ ![image](https://github.com/user-attachments/assets/4bb214ca-1132-4e63-9390-d6eaddab50be)
7
+
8
+
9
+
6
10
  A small Model Context Protocol (MCP) server that exposes your Strava athlete data to language-model tooling.
7
11
 
8
12
  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.
@@ -28,7 +32,7 @@ After installing, you can run the MCP server using the provided console script o
28
32
 
29
33
  Run via the console script (entry point defined in `pyproject.toml`):
30
34
 
31
- ```powershell
35
+ ```cmd
32
36
  strava-activity-mcp-server
33
37
  ```
34
38
 
@@ -52,27 +56,21 @@ This server requires Strava OAuth credentials to access athlete data. You will n
52
56
  Steps:
53
57
 
54
58
  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.
55
- 2. Open the authorization URL produced by the `strava://auth/url` tool (see Tools below) in a browser to obtain an authorization code.
56
- 3. Exchange the code for tokens using `strava://auth/token` or use the included helper to save refresh/access tokens to your environment.
59
+ 2. Open the authorization URL produced by the `strava://auth/url` tool (see IMAGE below) in a browser to obtain an authorization code.
57
60
 
58
- For local testing you can also manually set the environment variables before running the server:
61
+ ![auth](https://github.com/user-attachments/assets/a348ccc7-a4be-49fb-8f79-b88f9d80cfc9)
59
62
 
60
- ```powershell
61
- $env:STRAVA_CLIENT_ID = "<your client id>";
62
- $env:STRAVA_CLIENT_SECRET = "<your client secret>";
63
- $env:STRAVA_REFRESH_TOKEN = "<your refresh token>";
64
- strava-activity-mcp-server
65
- ```
63
+ 3. Copy the code from the redirected URL (Image below) or use the included helper to save refresh/access tokens to your environment.
64
+
65
+ ![code](https://github.com/user-attachments/assets/0bb54edb-c9f9-4416-8fb2-c7e0a38d11c9)
66
66
 
67
- Note: Keep secrets out of version control. Use a `.env` file and a tool such as `direnv` or your system secrets manager for convenience.
68
67
 
69
68
  ## Exposed Tools (what the server provides)
70
69
 
71
70
  The MCP server exposes the following tools (tool IDs shown):
72
71
 
73
72
  - `strava://auth/url` — Build the Strava OAuth authorization URL. Input: `client_id` (int). Output: URL string to open in a browser.
74
- - `strava://auth/token` — Exchange an authorization code for access + refresh tokens. Inputs: `code` (str), `client_id` (int), `client_secret` (str). Output: token dict (with `access_token`, `refresh_token`).
75
- - `strava://athlete/stats` — Fetch recent athlete activities. Input: `token` (str). Output: JSON with activity list.
73
+ - `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.
76
74
 
77
75
  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.
78
76
 
@@ -81,28 +79,27 @@ These tools map to the functions implemented in `src/strava_activity_mcp_server/
81
79
  1) Get an authorization URL and retrieve tokens
82
80
 
83
81
  - Call `strava://auth/url` with your `client_id` and open the returned URL in your browser.
84
- - After authorizing, Strava will provide a `code`. Call `strava://auth/token` with `code`, `client_id`, and `client_secret` to receive `access_token` and `refresh_token`.
82
+ - After authorizing, Strava will provide a `code`.
85
83
 
86
84
  2) Fetch recent activities
87
85
 
88
86
  - Use `strava://athlete/stats` with a valid access token. If the access token is expired, use the refresh flow to get a new access token.
89
87
 
90
- ## Developer notes
91
-
92
- - The package entry point calls `mcp.run()` which runs the MCP server. If you want to change transport or logging settings, modify `src/strava_activity_mcp_server/__init__.py` or `strava_activity_mcp_server.py`.
93
- - The code uses the `requests` library for HTTP calls.
94
-
95
-
96
88
  ### Client config example and quick inspector test
97
89
 
98
- Any MCP-capable client can launch the server using a config similar to the following (example file often called `config.json`):
90
+ Any MCP-capable client can launch the server using a config similar to the following (example file often called `config.json`. Be sure to enter your values here):
99
91
 
100
92
  ```json
101
93
  {
102
- "command": "uvx",
103
- "args": [
104
- "strava-activity-mcp-server"
105
- ]
94
+ "command": "uvx",
95
+ "args": [
96
+ "strava-activity-mcp-server"
97
+ ],
98
+ "env": {
99
+ "STRAVA_CLIENT_ID": "12345",
100
+ "STRAVA_CLIENT_SECRET": "e1234a12d12345f12c1f12345a123bba1d12c1",
101
+ "STRAVA_REFRESH_TOKEN": "1a123eda1cfd12345678987db2db1bda234c38"
102
+ }
106
103
  }
107
104
  ```
108
105
 
@@ -130,3 +127,4 @@ This project is licensed under the GNU GENERAL PUBLIC LICENSE — see the `LICEN
130
127
 
131
128
 
132
129
 
130
+
@@ -1,18 +1,18 @@
1
- [project]
2
- name = "strava-activity-mcp-server"
3
- version = "0.1.7"
4
- description = "Trying to implement environment variables for client_id"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = [
8
- "build>=1.3.0",
9
- "mcp[cli]>=1.16.0",
10
- "twine>=6.2.0",
11
- ]
12
-
13
- [project.scripts]
14
- strava-activity-mcp-server = "strava_activity_mcp_server:main"
15
-
16
- [build-system]
17
- requires = ["hatchling"]
18
- build-backend = "hatchling.build"
1
+ [project]
2
+ name = "strava-activity-mcp-server"
3
+ version = "0.1.9"
4
+ description = "STRAVA ACTIVITY MCP SERVER"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "build>=1.3.0",
9
+ "mcp[cli]>=1.16.0",
10
+ "twine>=6.2.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ strava-activity-mcp-server = "strava_activity_mcp_server:main"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
@@ -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()
@@ -0,0 +1,100 @@
1
+ import sys
2
+ import os
3
+ from mcp.server.fastmcp import FastMCP # Import FastMCP, the quickstart server base
4
+ mcp = FastMCP("Strava") # Initialize an MCP server instance with a descriptive name
5
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
+ import requests
7
+ import urllib.parse
8
+
9
+ @mcp.tool("strava://auth/url")
10
+
11
+ def get_auth_url(client_id: int | None = None):
12
+ """Return the Strava OAuth authorization URL. If client_id is not provided,
13
+ read it from the STRAVA_CLIENT_ID environment variable."""
14
+ if client_id is None:
15
+ client_id_env = os.getenv("STRAVA_CLIENT_ID")
16
+ if not client_id_env:
17
+ return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
18
+ try:
19
+ client_id = int(client_id_env)
20
+ except ValueError:
21
+ return {"error": "STRAVA_CLIENT_ID must be an integer"}
22
+
23
+ params = {
24
+ "client_id": client_id,
25
+ "response_type": "code",
26
+ "redirect_uri": "https://developers.strava.com/oauth2-redirect/",
27
+ "approval_prompt": "force",
28
+ "scope": "read,activity:read_all",
29
+ }
30
+ return "https://www.strava.com/oauth/authorize?" + urllib.parse.urlencode(params)
31
+
32
+
33
+
34
+ @mcp.tool("strava://athlete/stats")
35
+ def get_athlete_stats(
36
+ code: str,
37
+ client_id: int | None = None,
38
+ client_secret: str | None = None,) -> dict:
39
+
40
+ #'''Exchange an authorization code for access + refresh tokens.'''
41
+ if not code:
42
+ return {"error": "authorization code is required"}
43
+
44
+ if client_id is None:
45
+ client_id_env = os.getenv("STRAVA_CLIENT_ID")
46
+ if not client_id_env:
47
+ return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
48
+ try:
49
+ client_id = int(client_id_env)
50
+ except ValueError:
51
+ return {"error": "STRAVA_CLIENT_ID must be an integer"}
52
+
53
+ if client_secret is None:
54
+ client_secret_env = os.getenv("STRAVA_CLIENT_SECRET")
55
+ if not client_secret_env:
56
+ return {"error": "STRAVA_CLIENT_SECRET environment variable is not set"}
57
+ try:
58
+ client_secret = str(client_secret_env)
59
+ except ValueError:
60
+ return {"error": "STRAVA_CLIENT_SECRET must be a string"}
61
+
62
+
63
+ resp = requests.post(
64
+ "https://www.strava.com/oauth/token",
65
+ data={
66
+ "client_id": client_id,
67
+ "client_secret": client_secret,
68
+ "code": code,
69
+ "grant_type": "authorization_code",
70
+ },
71
+ )
72
+ try:
73
+ resp.raise_for_status()
74
+ except requests.HTTPError:
75
+ return {"error": "token request failed", "status_code": resp.status_code, "response": resp.text}
76
+ except Exception as e:
77
+ return {"error": "token request failed", "status_code": resp.status_code, "response": resp.text, "error": str(e)}
78
+
79
+ tokens = resp.json()
80
+ # Print tokens for debugging (optional)
81
+ print(tokens)
82
+
83
+ access_token = tokens.get("access_token")
84
+ refresh_token = tokens.get("refresh_token")
85
+
86
+
87
+ #return {"tokens": tokens, "access_token": access_token, "refresh_token": refresh_token}
88
+
89
+ url = "https://www.strava.com/api/v3/athlete/activities?per_page=60"
90
+ headers = {
91
+ "accept": "application/json",
92
+ "authorization": f"Bearer {access_token}"
93
+ }
94
+
95
+ response = requests.get(url, headers=headers)
96
+
97
+ return response.json()
98
+
99
+ if __name__ == "__main__":
100
+ mcp.run(transport="stdio") # Run the server, using standard input/output for communication
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
@@ -1031,7 +1031,7 @@ wheels = [
1031
1031
 
1032
1032
  [[package]]
1033
1033
  name = "strava-activity-mcp-server"
1034
- version = "0.1.7"
1034
+ version = "0.1.9"
1035
1035
  source = { editable = "." }
1036
1036
  dependencies = [
1037
1037
  { name = "build" },
@@ -1,222 +0,0 @@
1
- import sys
2
- import os
3
- from mcp.server.fastmcp import FastMCP # Import FastMCP, the quickstart server base
4
- mcp = FastMCP("Strava") # Initialize an MCP server instance with a descriptive name
5
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
6
- import requests
7
- import urllib.parse
8
-
9
- @mcp.tool("strava://auth/url")
10
-
11
- def get_auth_url(client_id: int | None = None):
12
- """Return the Strava OAuth authorization URL. If client_id is not provided,
13
- read it from the STRAVA_CLIENT_ID environment variable."""
14
- if client_id is None:
15
- client_id_env = os.getenv("STRAVA_CLIENT_ID")
16
- if not client_id_env:
17
- return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
18
- try:
19
- client_id = int(client_id_env)
20
- except ValueError:
21
- return {"error": "STRAVA_CLIENT_ID must be an integer"}
22
-
23
- params = {
24
- "client_id": client_id,
25
- "response_type": "code",
26
- "redirect_uri": "https://developers.strava.com/oauth2-redirect/",
27
- "approval_prompt": "force",
28
- "scope": "read,activity:read_all",
29
- }
30
- return "https://www.strava.com/oauth/authorize?" + urllib.parse.urlencode(params)
31
-
32
-
33
-
34
- @mcp.tool("strava://auth/token")
35
- def exchange_code_for_token(
36
- code: str,
37
- client_id: int | None = None,
38
- client_secret: str | None = None,
39
- ) -> dict:
40
- """Exchange an authorization code for access + refresh tokens."""
41
- if not code:
42
- return {"error": "authorization code is required"}
43
-
44
- if client_id is None:
45
- client_id_env = os.getenv("STRAVA_CLIENT_ID")
46
- if not client_id_env:
47
- return {"error": "STRAVA_CLIENT_ID environment variable is not set"}
48
- try:
49
- client_id = int(client_id_env)
50
- except ValueError:
51
- return {"error": "STRAVA_CLIENT_ID must be an integer"}
52
-
53
- if client_secret is None:
54
- client_secret_env = os.getenv("STRAVA_CLIENT_SECRET")
55
- if not client_secret_env:
56
- return {"error": "STRAVA_CLIENT_SECRET environment variable is not set"}
57
- try:
58
- client_secret = str(client_secret_env)
59
- except ValueError:
60
- return {"error": "STRAVA_CLIENT_SECRET must be a string"}
61
-
62
- resp = requests.post(
63
- "https://www.strava.com/oauth/token",
64
- data={
65
- "client_id": client_id,
66
- "client_secret": client_secret,
67
- "code": code,
68
- "grant_type": "authorization_code",
69
- },
70
- )
71
- try:
72
- resp.raise_for_status()
73
- except requests.HTTPError:
74
- return {"error": "token request failed", "status_code": resp.status_code, "response": resp.text}
75
-
76
- tokens = resp.json()
77
- # Print tokens for debugging (optional)
78
- print(tokens)
79
-
80
- access_token = tokens.get("access_token")
81
- refresh_token = tokens.get("refresh_token")
82
-
83
- return {"tokens": tokens, "access_token": access_token, "refresh_token": refresh_token}
84
-
85
-
86
- def refresh_access_token(
87
- refresh_token: str,
88
- client_id: int,
89
- client_secret: str,
90
- ) -> dict:
91
- """Refresh an access token using a refresh token."""
92
- if not refresh_token:
93
- return {"error": "refresh_token is required"}
94
-
95
- resp = requests.post(
96
- "https://www.strava.com/oauth/token",
97
- data={
98
- "client_id": client_id,
99
- "client_secret": client_secret,
100
- "grant_type": "refresh_token",
101
- "refresh_token": refresh_token,
102
- },
103
- )
104
- try:
105
- resp.raise_for_status()
106
- except requests.HTTPError:
107
- return {"error": "refresh request failed", "status_code": resp.status_code, "response": resp.text}
108
-
109
- new_tokens = resp.json()
110
- # Print new tokens for debugging (optional)
111
- print(new_tokens)
112
- return new_tokens
113
-
114
-
115
- @mcp.tool("strava://athlete/stats")
116
- def _get_env_client_credentials() -> tuple[int | None, str | None]:
117
- """Read client id and secret from environment and return (client_id, client_secret).
118
-
119
- client_id will be returned as int if present and valid, otherwise None.
120
- """
121
- client_id = None
122
- client_secret = os.getenv("STRAVA_CLIENT_SECRET")
123
- client_id_env = os.getenv("STRAVA_CLIENT_ID")
124
- if client_id_env:
125
- try:
126
- client_id = int(client_id_env)
127
- except ValueError:
128
- client_id = None
129
- return client_id, client_secret
130
-
131
-
132
- def _ensure_access_token(token_or_tokens: object) -> tuple[str | None, dict | None]:
133
- """Given either an access token string or the token dict returned by the token endpoints,
134
- return a tuple (access_token, tokens_dict).
135
-
136
- If a dict is provided and contains no valid access_token but has a refresh_token,
137
- attempt to refresh using env client credentials. Returns (access_token, tokens_dict) or (None, None)
138
- on failure.
139
- """
140
- # If token_or_tokens is a string, assume it's an access token.
141
- if isinstance(token_or_tokens, str):
142
- return token_or_tokens, None
143
-
144
- # If it's a dict-like object, try to find access_token
145
- if isinstance(token_or_tokens, dict):
146
- access_token = token_or_tokens.get("access_token")
147
- if access_token:
148
- return access_token, token_or_tokens
149
-
150
- # try refresh flow
151
- refresh_token = token_or_tokens.get("refresh_token")
152
- client_id = token_or_tokens.get("client_id")
153
- client_secret = token_or_tokens.get("client_secret")
154
-
155
- # fallback to env vars if client id/secret not in the dict
156
- if not client_id or not client_secret:
157
- env_client_id, env_client_secret = _get_env_client_credentials()
158
- if not client_id:
159
- client_id = env_client_id
160
- if not client_secret:
161
- client_secret = env_client_secret
162
-
163
- if refresh_token and client_id and client_secret:
164
- try:
165
- new_tokens = refresh_access_token(refresh_token, int(client_id), client_secret)
166
- except Exception as e:
167
- print(f"refresh failed: {e}")
168
- return None, None
169
-
170
- access_token = new_tokens.get("access_token")
171
- return access_token, new_tokens
172
-
173
- return None, None
174
-
175
-
176
- @mcp.tool("strava://athlete/stats")
177
- def get_athlete_stats(token: object) -> object:
178
- """Retrieve athlete activities using either an access token string or a token dict.
179
-
180
- If a token dict is provided and the access token is missing/expired, the function will
181
- attempt to refresh it (one attempt) using provided refresh token and client credentials
182
- (falling back to STRAVA_CLIENT_ID/STRAVA_CLIENT_SECRET environment variables).
183
- """
184
- access_token, tokens_dict = _ensure_access_token(token)
185
- if not access_token:
186
- return {"error": "Could not obtain an access token"}
187
-
188
- url = "https://www.strava.com/api/v3/athlete/activities?per_page=60"
189
- headers = {
190
- "accept": "application/json",
191
- "authorization": f"Bearer {access_token}"
192
- }
193
-
194
- response = requests.get(url, headers=headers)
195
-
196
- # If unauthorized, try one refresh if we have a refresh token available
197
- if response.status_code == 401 and isinstance(token, dict):
198
- refresh_token = token.get("refresh_token") or (tokens_dict or {}).get("refresh_token")
199
- if refresh_token:
200
- client_id = token.get("client_id") or (tokens_dict or {}).get("client_id")
201
- client_secret = token.get("client_secret") or (tokens_dict or {}).get("client_secret")
202
- if not client_id or not client_secret:
203
- env_client_id, env_client_secret = _get_env_client_credentials()
204
- client_id = client_id or env_client_id
205
- client_secret = client_secret or env_client_secret
206
-
207
- if client_id and client_secret:
208
- new_tokens = refresh_access_token(refresh_token, int(client_id), client_secret)
209
- new_access = new_tokens.get("access_token")
210
- if new_access:
211
- headers["authorization"] = f"Bearer {new_access}"
212
- response = requests.get(url, headers=headers)
213
-
214
- try:
215
- response.raise_for_status()
216
- except requests.HTTPError:
217
- return {"error": "request failed", "status_code": response.status_code, "response": response.text}
218
-
219
- return response.json()
220
-
221
- if __name__ == "__main__":
222
- mcp.run(transport="stdio") # Run the server, using standard input/output for communication