Spaces:
Running
Running
burtenshaw
commited on
Commit
Β·
4186a82
1
Parent(s):
2e832d9
update sse configuration
Browse files- DEPLOYMENT_GUIDE.md +146 -0
- run_mcp_server.py +12 -0
- strava_mcp/gradio_server.py +51 -27
- test_mcp_connection.py +111 -0
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:
|
| 123 |
-
after:
|
| 124 |
-
page:
|
| 125 |
-
per_page:
|
| 126 |
-
) ->
|
| 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(
|
|
|
|
|
|
|
| 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:
|
| 163 |
-
include_all_efforts:
|
| 164 |
-
) ->
|
| 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:
|
| 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(
|
| 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:
|
| 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(
|
| 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.
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 272 |
-
gr.
|
|
|
|
|
|
|
| 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.
|
| 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
|
| 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()
|