burtenshaw commited on
Commit
4186a82
Β·
1 Parent(s): 2e832d9

update sse configuration

Browse files
DEPLOYMENT_GUIDE.md ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Strava MCP Deployment Guide
2
+
3
+ This repository contains **two different server implementations** for different use cases:
4
+
5
+ ## 🎯 **Deployment Options**
6
+
7
+ ### 1. 🌐 **Hugging Face Spaces** (Web Interface + MCP SSE)
8
+ **Use this for**: Web-based interface accessible via browser AND MCP Server-Sent Events endpoint
9
+
10
+ **Entry Point**: `app.py` β†’ `strava_mcp/gradio_server.py`
11
+
12
+ **Features**:
13
+ - Web-based Gradio interface with tabs
14
+ - Manual OAuth token input
15
+ - Step-by-step authentication guide
16
+ - **MCP SSE endpoint at**: `https://your-space.hf.space/gradio_api/mcp/sse`
17
+ - Perfect for sharing, demonstrations, and LLM integration
18
+
19
+ **How to Deploy**:
20
+ 1. Upload your code to Hugging Face Spaces
21
+ 2. Set environment variables in Space settings:
22
+ ```bash
23
+ STRAVA_CLIENT_ID=your_client_id
24
+ STRAVA_CLIENT_SECRET=your_client_secret
25
+ ```
26
+ 3. Use the web interface to authenticate and get activities
27
+ 4. **For MCP clients**: Connect to `https://your-space.hf.space/gradio_api/mcp/sse`
28
+
29
+ ### 2. πŸ–₯️ **Claude Desktop** (MCP Integration)
30
+ **Use this for**: Direct integration with Claude Desktop application
31
+
32
+ **Entry Point**: `run_mcp_server.py` β†’ `strava_mcp/main.py` β†’ `strava_mcp/server.py`
33
+
34
+ **Features**:
35
+ - Native MCP protocol support
36
+ - Direct integration with Claude Desktop
37
+ - Automatic OAuth flow (when possible)
38
+ - Tools available directly in Claude conversations
39
+
40
+ **How to Setup**:
41
+ 1. Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "strava": {
46
+ "command": "python",
47
+ "args": ["/path/to/strava-mcp/run_mcp_server.py"],
48
+ "env": {
49
+ "STRAVA_CLIENT_ID": "your_client_id",
50
+ "STRAVA_CLIENT_SECRET": "your_client_secret",
51
+ "STRAVA_REFRESH_TOKEN": "your_refresh_token"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## πŸ”§ **Key Differences**
59
+
60
+ | Feature | Hugging Face Spaces | Claude Desktop |
61
+ |---------|-------------------|----------------|
62
+ | **Interface** | Web browser + SSE endpoint | Claude Desktop app |
63
+ | **Authentication** | Manual token input | Environment variables |
64
+ | **OAuth Flow** | Manual (with helper) | Can be automatic |
65
+ | **Access** | Public/shareable | Local only |
66
+ | **Protocol** | HTTP/Gradio + MCP/SSE | MCP/stdio |
67
+ | **Use Case** | Demos, sharing, LLM integration | Personal use |
68
+ | **MCP Endpoint** | `/gradio_api/mcp/sse` | stdio transport |
69
+
70
+ ## ❌ **Common Issues Fixed**
71
+
72
+ ### ASGI Protocol Errors
73
+ **Problem**: Trying to run both Gradio and MCP server simultaneously caused ASGI conflicts.
74
+
75
+ **Solution**: Separated the implementations:
76
+ - Hugging Face Spaces: Uses only Gradio (removed `mcp_server=True`)
77
+ - Claude Desktop: Uses only MCP server with stdio transport
78
+
79
+ ### Scope Permission Errors
80
+ **Problem**: Refresh tokens created without `activity:read` scope.
81
+
82
+ **Solution**:
83
+ - Updated OAuth URLs to include correct scopes: `read_all,activity:read,activity:read_all,profile:read_all`
84
+ - Added clear error messages and guidance
85
+ - Created OAuth Helper with step-by-step instructions
86
+
87
+ ## πŸ“ **File Structure**
88
+
89
+ ```
90
+ strava-mcp/
91
+ β”œβ”€β”€ app.py # 🌐 HF Spaces entry point
92
+ β”œβ”€β”€ run_mcp_server.py # πŸ–₯️ Claude Desktop entry point
93
+ β”œβ”€β”€ claude_desktop_config_example.json # Configuration example
94
+ β”œβ”€β”€ strava_mcp/
95
+ β”‚ β”œβ”€β”€ gradio_server.py # 🌐 Web interface (Gradio)
96
+ β”‚ β”œβ”€β”€ server.py # πŸ–₯️ MCP server implementation
97
+ β”‚ β”œβ”€β”€ main.py # πŸ–₯️ MCP server runner
98
+ β”‚ β”œβ”€β”€ api.py # Strava API client
99
+ β”‚ β”œβ”€β”€ auth.py # OAuth authentication
100
+ β”‚ β”œβ”€β”€ service.py # Business logic
101
+ β”‚ β”œβ”€β”€ models.py # Data models
102
+ β”‚ └── config.py # Settings
103
+ └── check_token_scopes.py # πŸ”§ Diagnostic tool
104
+ ```
105
+
106
+ ## πŸš€ **Quick Start**
107
+
108
+ ### For Hugging Face Spaces:
109
+ ```bash
110
+ # Deploy to HF Spaces with app.py as entry point
111
+ # Set STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET in Space settings
112
+ # Use web interface to authenticate
113
+ ```
114
+
115
+ ### For Claude Desktop:
116
+ ```bash
117
+ # 1. Get your refresh token
118
+ python check_token_scopes.py CLIENT_ID CLIENT_SECRET REFRESH_TOKEN
119
+
120
+ # 2. Add to Claude Desktop config
121
+ # 3. Restart Claude Desktop
122
+ # 4. Use Strava tools in conversations
123
+ ```
124
+
125
+ ## πŸ†˜ **Troubleshooting**
126
+
127
+ ### "ASGI Protocol Error"
128
+ - **Cause**: Trying to use `mcp_server=True` in Gradio
129
+ - **Fix**: Use separate deployments (this is now fixed)
130
+
131
+ ### "activity:read_permission missing"
132
+ - **Cause**: Refresh token created without correct scopes
133
+ - **Fix**: Get new token using OAuth Helper with correct scopes
134
+
135
+ ### "Service not initialized"
136
+ - **Cause**: Missing environment variables
137
+ - **Fix**: Set `STRAVA_CLIENT_ID`, `STRAVA_CLIENT_SECRET`, and `STRAVA_REFRESH_TOKEN`
138
+
139
+ ## πŸŽ‰ **Success!**
140
+
141
+ After these fixes:
142
+ - βœ… Hugging Face Spaces deployment works without ASGI errors
143
+ - βœ… Claude Desktop integration works with proper MCP protocol
144
+ - βœ… Clear separation between web interface and MCP server
145
+ - βœ… Better error messages and authentication guidance
146
+ - βœ… Comprehensive documentation and examples
run_mcp_server.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Entry point for running the MCP server locally (for Claude Desktop integration).
4
+ This is separate from the Hugging Face Spaces deployment.
5
+ """
6
+
7
+ from strava_mcp.main import main
8
+
9
+ if __name__ == "__main__":
10
+ print("πŸš€ Starting Strava MCP Server for Claude Desktop...")
11
+ print("πŸ’‘ For Hugging Face Spaces, use app.py instead")
12
+ main()
strava_mcp/gradio_server.py CHANGED
@@ -119,28 +119,35 @@ def get_authorization_url() -> str:
119
 
120
 
121
  async def get_user_activities(
122
- before: Optional[int] = None,
123
- after: Optional[int] = None,
124
- page: int = 1,
125
- per_page: int = 30,
126
- ) -> List[Dict]:
127
  """Get the authenticated user's activities from Strava.
128
 
129
  Args:
130
- before: An epoch timestamp for filtering activities before a certain time
131
- after: An epoch timestamp for filtering activities after a certain time
132
- page: Page number (default: 1)
133
- per_page: Number of items per page (default: 30, max: 200)
134
 
135
  Returns:
136
  List of activities with details like name, distance, moving time, etc.
137
  """
 
 
 
 
 
138
  try:
139
  await initialize_service()
140
  if service is None:
141
  raise ValueError("Service not initialized")
142
 
143
- activities = await service.get_activities(before, after, page, per_page)
 
 
144
  return [activity.model_dump() for activity in activities]
145
  except Exception as e:
146
  logger.error(f"Error getting user activities: {str(e)}")
@@ -159,24 +166,27 @@ async def get_user_activities(
159
 
160
 
161
  async def get_activity_details(
162
- activity_id: int,
163
- include_all_efforts: bool = False,
164
- ) -> Dict:
165
  """Get detailed information about a specific activity.
166
 
167
  Args:
168
- activity_id: The unique ID of the activity
169
- include_all_efforts: Whether to include all segment efforts (default: False)
170
 
171
  Returns:
172
  Detailed activity information including stats, segments, and metadata
173
  """
 
 
 
174
  try:
175
  await initialize_service()
176
  if service is None:
177
  raise ValueError("Service not initialized")
178
 
179
- activity = await service.get_activity(activity_id, include_all_efforts)
180
  return activity.model_dump()
181
  except Exception as e:
182
  logger.error(f"Error getting activity details: {str(e)}")
@@ -192,21 +202,23 @@ async def get_activity_details(
192
  raise gr.Error(f"Failed to get activity details: {error_msg}")
193
 
194
 
195
- async def get_activity_segments(activity_id: int) -> List[Dict]:
196
  """Get segment efforts for a specific activity.
197
 
198
  Args:
199
- activity_id: The unique ID of the activity
200
 
201
  Returns:
202
  List of segment efforts with performance data and rankings
203
  """
 
 
204
  try:
205
  await initialize_service()
206
  if service is None:
207
  raise ValueError("Service not initialized")
208
 
209
- segments = await service.get_activity_segments(activity_id)
210
  return [segment.model_dump() for segment in segments]
211
  except Exception as e:
212
  logger.error(f"Error getting activity segments: {str(e)}")
@@ -254,10 +266,18 @@ def create_interface():
254
  activities_interface = gr.Interface(
255
  fn=get_user_activities,
256
  inputs=[
257
- gr.Number(label="Before (epoch timestamp)", value=None, precision=0),
258
- gr.Number(label="After (epoch timestamp)", value=None, precision=0),
259
- gr.Number(label="Page", value=1, precision=0),
260
- gr.Number(label="Per Page", value=30, precision=0),
 
 
 
 
 
 
 
 
261
  ],
262
  outputs=gr.JSON(label="Activities"),
263
  title="πŸ“Š Get User Activities",
@@ -268,8 +288,10 @@ def create_interface():
268
  details_interface = gr.Interface(
269
  fn=get_activity_details,
270
  inputs=[
271
- gr.Number(label="Activity ID", precision=0),
272
- gr.Checkbox(label="Include All Efforts", value=False),
 
 
273
  ],
274
  outputs=gr.JSON(label="Activity Details"),
275
  title="πŸ” Get Activity Details",
@@ -280,7 +302,7 @@ def create_interface():
280
  segments_interface = gr.Interface(
281
  fn=get_activity_segments,
282
  inputs=[
283
- gr.Number(label="Activity ID", precision=0),
284
  ],
285
  outputs=gr.JSON(label="Activity Segments"),
286
  title="πŸƒ Get Activity Segments",
@@ -315,12 +337,14 @@ def main():
315
 
316
  demo = create_interface()
317
 
318
- # Launch with MCP server enabled for Hugging Face Spaces
319
  demo.launch(
320
  mcp_server=True,
321
  server_name="0.0.0.0",
322
  server_port=7860, # Standard port for HF Spaces
323
  share=False,
 
 
324
  )
325
 
326
 
 
119
 
120
 
121
  async def get_user_activities(
122
+ before: str = "",
123
+ after: str = "",
124
+ page: str = "1",
125
+ per_page: str = "30",
126
+ ) -> list[dict]:
127
  """Get the authenticated user's activities from Strava.
128
 
129
  Args:
130
+ before (str): An epoch timestamp for filtering activities before a certain time (leave empty for no filter)
131
+ after (str): An epoch timestamp for filtering activities after a certain time (leave empty for no filter)
132
+ page (str): Page number (default: 1)
133
+ per_page (str): Number of items per page (default: 30, max: 200)
134
 
135
  Returns:
136
  List of activities with details like name, distance, moving time, etc.
137
  """
138
+ # Convert string parameters to appropriate types
139
+ before_int = int(before) if before.strip() else None
140
+ after_int = int(after) if after.strip() else None
141
+ page_int = int(page) if page.strip() else 1
142
+ per_page_int = int(per_page) if per_page.strip() else 30
143
  try:
144
  await initialize_service()
145
  if service is None:
146
  raise ValueError("Service not initialized")
147
 
148
+ activities = await service.get_activities(
149
+ before_int, after_int, page_int, per_page_int
150
+ )
151
  return [activity.model_dump() for activity in activities]
152
  except Exception as e:
153
  logger.error(f"Error getting user activities: {str(e)}")
 
166
 
167
 
168
  async def get_activity_details(
169
+ activity_id: str,
170
+ include_all_efforts: str = "false",
171
+ ) -> dict:
172
  """Get detailed information about a specific activity.
173
 
174
  Args:
175
+ activity_id (str): The unique ID of the activity
176
+ include_all_efforts (str): Whether to include all segment efforts (true/false, default: false)
177
 
178
  Returns:
179
  Detailed activity information including stats, segments, and metadata
180
  """
181
+ # Convert string parameters to appropriate types
182
+ activity_id_int = int(activity_id)
183
+ include_efforts_bool = include_all_efforts.lower().strip() == "true"
184
  try:
185
  await initialize_service()
186
  if service is None:
187
  raise ValueError("Service not initialized")
188
 
189
+ activity = await service.get_activity(activity_id_int, include_efforts_bool)
190
  return activity.model_dump()
191
  except Exception as e:
192
  logger.error(f"Error getting activity details: {str(e)}")
 
202
  raise gr.Error(f"Failed to get activity details: {error_msg}")
203
 
204
 
205
+ async def get_activity_segments(activity_id: str) -> list[dict]:
206
  """Get segment efforts for a specific activity.
207
 
208
  Args:
209
+ activity_id (str): The unique ID of the activity
210
 
211
  Returns:
212
  List of segment efforts with performance data and rankings
213
  """
214
+ # Convert string parameter to appropriate type
215
+ activity_id_int = int(activity_id)
216
  try:
217
  await initialize_service()
218
  if service is None:
219
  raise ValueError("Service not initialized")
220
 
221
+ segments = await service.get_activity_segments(activity_id_int)
222
  return [segment.model_dump() for segment in segments]
223
  except Exception as e:
224
  logger.error(f"Error getting activity segments: {str(e)}")
 
266
  activities_interface = gr.Interface(
267
  fn=get_user_activities,
268
  inputs=[
269
+ gr.Textbox(
270
+ label="Before (epoch timestamp)",
271
+ value="",
272
+ placeholder="Leave empty for no filter",
273
+ ),
274
+ gr.Textbox(
275
+ label="After (epoch timestamp)",
276
+ value="",
277
+ placeholder="Leave empty for no filter",
278
+ ),
279
+ gr.Textbox(label="Page", value="1", placeholder="1"),
280
+ gr.Textbox(label="Per Page", value="30", placeholder="30"),
281
  ],
282
  outputs=gr.JSON(label="Activities"),
283
  title="πŸ“Š Get User Activities",
 
288
  details_interface = gr.Interface(
289
  fn=get_activity_details,
290
  inputs=[
291
+ gr.Textbox(label="Activity ID", placeholder="Enter activity ID"),
292
+ gr.Textbox(
293
+ label="Include All Efforts", value="false", placeholder="true or false"
294
+ ),
295
  ],
296
  outputs=gr.JSON(label="Activity Details"),
297
  title="πŸ” Get Activity Details",
 
302
  segments_interface = gr.Interface(
303
  fn=get_activity_segments,
304
  inputs=[
305
+ gr.Textbox(label="Activity ID", placeholder="Enter activity ID"),
306
  ],
307
  outputs=gr.JSON(label="Activity Segments"),
308
  title="πŸƒ Get Activity Segments",
 
337
 
338
  demo = create_interface()
339
 
340
+ # Launch for Hugging Face Spaces with MCP server enabled via SSE
341
  demo.launch(
342
  mcp_server=True,
343
  server_name="0.0.0.0",
344
  server_port=7860, # Standard port for HF Spaces
345
  share=False,
346
+ max_request_size=50 * 1024 * 1024, # 50MB max request size
347
+ max_header_size=32768, # 32KB max header size
348
  )
349
 
350
 
test_mcp_connection.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify MCP SSE endpoint connectivity.
4
+ This helps debug connection issues with the Strava MCP server.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import sys
10
+ from typing import Optional
11
+
12
+ import httpx
13
+
14
+
15
+ async def test_mcp_sse_connection(base_url: str) -> bool:
16
+ """Test connection to the MCP SSE endpoint.
17
+
18
+ Args:
19
+ base_url: The base URL of the Hugging Face Space (e.g., https://your-space.hf.space)
20
+
21
+ Returns:
22
+ True if connection successful, False otherwise
23
+ """
24
+ mcp_url = f"{base_url.rstrip('/')}/gradio_api/mcp/sse"
25
+ schema_url = f"{base_url.rstrip('/')}/gradio_api/mcp/schema"
26
+
27
+ print(f"πŸ” Testing MCP connection to: {mcp_url}")
28
+
29
+ # Test 1: Check if schema endpoint is accessible
30
+ print("\n1️⃣ Testing schema endpoint...")
31
+ try:
32
+ async with httpx.AsyncClient(timeout=30.0) as client:
33
+ response = await client.get(schema_url)
34
+
35
+ if response.status_code == 200:
36
+ print(f"βœ… Schema endpoint accessible: {response.status_code}")
37
+ try:
38
+ schema = response.json()
39
+ tools = schema.get("tools", [])
40
+ print(f"βœ… Found {len(tools)} MCP tools:")
41
+ for tool in tools:
42
+ name = tool.get("name", "unknown")
43
+ description = tool.get("description", "no description")
44
+ print(f" - {name}: {description}")
45
+ except Exception as e:
46
+ print(f"⚠️ Schema response not JSON: {e}")
47
+ else:
48
+ print(f"❌ Schema endpoint failed: {response.status_code} - {response.text}")
49
+ return False
50
+
51
+ except Exception as e:
52
+ print(f"❌ Schema endpoint error: {e}")
53
+ return False
54
+
55
+ # Test 2: Check if SSE endpoint is accessible
56
+ print(f"\n2️⃣ Testing SSE endpoint...")
57
+ try:
58
+ headers = {
59
+ "Accept": "text/event-stream",
60
+ "Cache-Control": "no-cache",
61
+ }
62
+
63
+ async with httpx.AsyncClient(timeout=30.0) as client:
64
+ # Try a simple connection test
65
+ response = await client.get(mcp_url, headers=headers)
66
+
67
+ if response.status_code == 200:
68
+ print(f"βœ… SSE endpoint accessible: {response.status_code}")
69
+ print(f"βœ… Content-Type: {response.headers.get('content-type', 'unknown')}")
70
+ return True
71
+ else:
72
+ print(f"❌ SSE endpoint failed: {response.status_code}")
73
+ print(f"❌ Response: {response.text}")
74
+ return False
75
+
76
+ except Exception as e:
77
+ print(f"❌ SSE endpoint error: {e}")
78
+ return False
79
+
80
+
81
+ def main():
82
+ """Main function to test MCP connection."""
83
+ if len(sys.argv) != 2:
84
+ print("Usage: python test_mcp_connection.py <hugging_face_space_url>")
85
+ print("Example: python test_mcp_connection.py https://burtenshaw-strava-mcp.hf.space")
86
+ sys.exit(1)
87
+
88
+ base_url = sys.argv[1]
89
+
90
+ print("πŸ§ͺ MCP SSE Connection Test")
91
+ print(f"🎯 Target: {base_url}")
92
+
93
+ success = asyncio.run(test_mcp_sse_connection(base_url))
94
+
95
+ if success:
96
+ print(f"\nπŸŽ‰ SUCCESS: MCP server is accessible!")
97
+ print(f"πŸ”— Your LLM can connect to: {base_url}/gradio_api/mcp/sse")
98
+ print(f"πŸ“‹ Schema available at: {base_url}/gradio_api/mcp/schema")
99
+ else:
100
+ print(f"\n❌ FAILED: MCP server connection failed")
101
+ print("πŸ’‘ Possible issues:")
102
+ print(" - Server not running or still starting up")
103
+ print(" - Authentication/credentials not configured")
104
+ print(" - Network connectivity issues")
105
+ print(" - Header size limitations")
106
+
107
+ sys.exit(0 if success else 1)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()