NiWaRe commited on
Commit
a643202
·
1 Parent(s): 2d37ec5

update app routing

Browse files
Files changed (2) hide show
  1. HUGGINGFACE_DEPLOYMENT.md +6 -3
  2. app.py +310 -77
HUGGINGFACE_DEPLOYMENT.md CHANGED
@@ -7,15 +7,18 @@ This repository is configured for deployment on Hugging Face Spaces as a Model C
7
  The application runs as a FastAPI server on port 7860 (HF Spaces default) with:
8
  - **Main landing page**: `/` - Serves the index.html with setup instructions
9
  - **Health check**: `/health` - Returns server status and W&B configuration
10
- - **MCP endpoint**: `/mcp` - Streamable HTTP transport endpoint for MCP (supports both regular HTTP and SSE upgrade)
 
 
11
 
12
  ## Key Changes for HF Spaces
13
 
14
  ### 1. app.py
15
  - Creates a FastAPI application that serves the landing page
16
- - Mounts the MCP server as a sub-application at `/mcp`
 
17
  - Configured to run on `0.0.0.0:7860` (HF Spaces requirement)
18
- - No need for static/templates directories - index.html is served directly
19
 
20
  ### 2. server.py
21
  - Exports necessary functions for HF Spaces initialization
 
7
  The application runs as a FastAPI server on port 7860 (HF Spaces default) with:
8
  - **Main landing page**: `/` - Serves the index.html with setup instructions
9
  - **Health check**: `/health` - Returns server status and W&B configuration
10
+ - **MCP endpoint**: `/mcp` - Streamable HTTP transport endpoint for MCP
11
+ - POST `/mcp` - Handles JSON-RPC requests (initialize, tools/list, tools/call)
12
+ - GET `/mcp` - SSE endpoint for server-initiated messages and long-lived connections
13
 
14
  ## Key Changes for HF Spaces
15
 
16
  ### 1. app.py
17
  - Creates a FastAPI application that serves the landing page
18
+ - Implements MCP streamable HTTP protocol directly (no FastMCP mounting issues)
19
+ - Handles both POST requests (JSON-RPC) and GET requests (SSE) at `/mcp`
20
  - Configured to run on `0.0.0.0:7860` (HF Spaces requirement)
21
+ - Sets W&B cache directories to `/tmp` to avoid permission issues
22
 
23
  ### 2. server.py
24
  - Exports necessary functions for HF Spaces initialization
app.py CHANGED
@@ -2,13 +2,17 @@
2
  """
3
  HuggingFace Spaces entry point for the Weights & Biases MCP Server.
4
 
5
- Simplified approach for HF Spaces deployment.
6
  """
7
 
8
  import os
9
  import sys
10
  import logging
 
 
11
  from pathlib import Path
 
 
12
 
13
  # Add the src directory to Python path
14
  sys.path.insert(0, str(Path(__file__).parent / "src"))
@@ -21,8 +25,8 @@ os.environ["HOME"] = "/tmp"
21
  os.environ["WANDB_SILENT"] = "True"
22
  os.environ["WEAVE_SILENT"] = "True"
23
 
24
- from fastapi import FastAPI
25
- from fastapi.responses import HTMLResponse
26
  from fastapi.middleware.cors import CORSMiddleware
27
  import uvicorn
28
 
@@ -31,7 +35,7 @@ logging.basicConfig(
31
  level=logging.INFO,
32
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
33
  )
34
- logger = logging.getLogger("huggingface-spaces-app")
35
 
36
  # Read the index.html file content
37
  INDEX_HTML_PATH = Path(__file__).parent / "index.html"
@@ -54,6 +58,115 @@ app.add_middleware(
54
  allow_headers=["*"],
55
  )
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  @app.get("/", response_class=HTMLResponse)
58
  async def index():
59
  """Serve the landing page."""
@@ -62,93 +175,213 @@ async def index():
62
  @app.get("/health")
63
  async def health():
64
  """Health check endpoint."""
65
- wandb_configured = bool(os.environ.get("WANDB_API_KEY"))
66
  return {
67
  "status": "healthy",
68
  "service": "wandb-mcp-server",
69
- "wandb_configured": wandb_configured
 
 
70
  }
71
 
72
- # Import and setup MCP functionality after basic app setup
73
- try:
74
- from wandb_mcp_server.server import (
75
- validate_and_get_api_key,
76
- setup_wandb_login,
77
- configure_wandb_logging,
78
- initialize_weave_tracing,
79
- create_mcp_server,
80
- ServerMCPArgs
81
- )
82
-
83
- # Setup W&B on import
84
- logger.info("Initializing W&B configuration...")
85
- configure_wandb_logging()
86
-
87
- args = ServerMCPArgs(
88
- transport="http",
89
- host="0.0.0.0",
90
- port=7860,
91
- wandb_api_key=os.environ.get("WANDB_API_KEY")
92
- )
93
 
94
  try:
95
- api_key = validate_and_get_api_key(args)
96
- setup_wandb_login(api_key)
97
- logger.info("W&B API configured successfully")
98
- except ValueError as e:
99
- logger.warning(f"W&B API key not configured: {e}")
100
- logger.warning("Server will start but W&B operations will fail")
101
-
102
- # Create MCP server instance
103
- mcp_server = create_mcp_server("http", "0.0.0.0", 7860)
104
-
105
- # Try to extract the FastAPI app from FastMCP and mount it
106
- if hasattr(mcp_server, 'app'):
107
- # Mount MCP app routes under /mcp
108
- from fastapi import APIRouter
109
- mcp_router = APIRouter(prefix="/mcp")
110
 
111
- # Copy routes from MCP app to our router
112
- if hasattr(mcp_server.app, 'routes'):
113
- for route in mcp_server.app.routes:
114
- if hasattr(route, 'endpoint') and hasattr(route, 'path'):
115
- mcp_router.add_api_route(
116
- path=route.path,
117
- endpoint=route.endpoint,
118
- methods=route.methods if hasattr(route, 'methods') else ["POST", "GET"],
119
- )
 
 
 
 
 
 
 
 
 
120
 
121
- app.include_router(mcp_router)
122
- logger.info("MCP routes mounted at /mcp")
123
- else:
124
- # Fallback: Create simple MCP endpoint
125
- @app.post("/mcp")
126
- async def mcp_endpoint():
127
- return {"message": "MCP server is running", "status": "ready"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- @app.get("/mcp")
130
- async def mcp_sse():
131
- from fastapi.responses import StreamingResponse
132
- import json
133
 
134
- async def event_stream():
135
- yield f"data: {json.dumps({'status': 'connected'})}\n\n"
 
 
 
 
 
 
 
136
 
137
- return StreamingResponse(event_stream(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- logger.warning("Using fallback MCP endpoints")
140
-
141
- except Exception as e:
142
- logger.error(f"Error setting up MCP server: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- # Provide fallback endpoints even if MCP setup fails
145
- @app.post("/mcp")
146
- async def mcp_fallback():
147
- return {"error": "MCP server initialization failed", "details": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- @app.get("/mcp")
150
- async def mcp_sse_fallback():
151
- return {"error": "MCP SSE not available", "details": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  def main():
154
  """Main entry point for HuggingFace Spaces."""
@@ -158,7 +391,7 @@ def main():
158
  logger.info(f"Starting server on {host}:{port}")
159
  logger.info("Landing page: /")
160
  logger.info("Health check: /health")
161
- logger.info("MCP endpoint: /mcp")
162
 
163
  uvicorn.run(
164
  app,
 
2
  """
3
  HuggingFace Spaces entry point for the Weights & Biases MCP Server.
4
 
5
+ This implements MCP streamable HTTP transport directly in FastAPI.
6
  """
7
 
8
  import os
9
  import sys
10
  import logging
11
+ import json
12
+ import asyncio
13
  from pathlib import Path
14
+ from typing import Dict, Any, Optional, List
15
+ from datetime import datetime
16
 
17
  # Add the src directory to Python path
18
  sys.path.insert(0, str(Path(__file__).parent / "src"))
 
25
  os.environ["WANDB_SILENT"] = "True"
26
  os.environ["WEAVE_SILENT"] = "True"
27
 
28
+ from fastapi import FastAPI, Request, Response, HTTPException
29
+ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
30
  from fastapi.middleware.cors import CORSMiddleware
31
  import uvicorn
32
 
 
35
  level=logging.INFO,
36
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
37
  )
38
+ logger = logging.getLogger("wandb-mcp-server")
39
 
40
  # Read the index.html file content
41
  INDEX_HTML_PATH = Path(__file__).parent / "index.html"
 
58
  allow_headers=["*"],
59
  )
60
 
61
+ # MCP server state
62
+ mcp_initialized = False
63
+ mcp_tools = {}
64
+ wandb_configured = False
65
+
66
+ @app.on_event("startup")
67
+ async def startup_event():
68
+ """Initialize W&B and MCP tools on startup."""
69
+ global mcp_initialized, mcp_tools, wandb_configured
70
+
71
+ logger.info("Starting Weights & Biases MCP Server on HuggingFace Spaces")
72
+
73
+ try:
74
+ # Import W&B components
75
+ from wandb_mcp_server.server import (
76
+ validate_and_get_api_key,
77
+ setup_wandb_login,
78
+ configure_wandb_logging,
79
+ initialize_weave_tracing,
80
+ ServerMCPArgs
81
+ )
82
+
83
+ # Configure W&B
84
+ configure_wandb_logging()
85
+
86
+ args = ServerMCPArgs(
87
+ transport="http",
88
+ host="0.0.0.0",
89
+ port=7860,
90
+ wandb_api_key=os.environ.get("WANDB_API_KEY")
91
+ )
92
+
93
+ try:
94
+ api_key = validate_and_get_api_key(args)
95
+ setup_wandb_login(api_key)
96
+ initialize_weave_tracing()
97
+ wandb_configured = True
98
+ logger.info("W&B API configured successfully")
99
+ except ValueError as e:
100
+ logger.warning(f"W&B API key not configured: {e}")
101
+ logger.warning("Server will start but W&B operations will fail")
102
+
103
+ # Import MCP tools
104
+ from wandb_mcp_server.mcp_tools.query_weave import (
105
+ QUERY_WEAVE_TRACES_TOOL_DESCRIPTION,
106
+ query_paginated_weave_traces
107
+ )
108
+ from wandb_mcp_server.mcp_tools.count_traces import (
109
+ COUNT_WEAVE_TRACES_TOOL_DESCRIPTION,
110
+ count_traces
111
+ )
112
+ from wandb_mcp_server.mcp_tools.query_wandb_gql import (
113
+ QUERY_WANDB_GQL_TOOL_DESCRIPTION,
114
+ query_paginated_wandb_gql
115
+ )
116
+ from wandb_mcp_server.mcp_tools.create_report import (
117
+ CREATE_WANDB_REPORT_TOOL_DESCRIPTION,
118
+ create_report
119
+ )
120
+ from wandb_mcp_server.mcp_tools.list_wandb_entities_projects import (
121
+ LIST_ENTITY_PROJECTS_TOOL_DESCRIPTION,
122
+ list_entity_projects
123
+ )
124
+ from wandb_mcp_server.mcp_tools.query_wandbot import (
125
+ WANDBOT_TOOL_DESCRIPTION,
126
+ query_wandbot_api
127
+ )
128
+
129
+ # Register tools with their descriptions
130
+ mcp_tools = {
131
+ "query_weave_traces_tool": {
132
+ "function": query_paginated_weave_traces,
133
+ "description": QUERY_WEAVE_TRACES_TOOL_DESCRIPTION,
134
+ "async": True
135
+ },
136
+ "count_weave_traces_tool": {
137
+ "function": count_traces,
138
+ "description": COUNT_WEAVE_TRACES_TOOL_DESCRIPTION,
139
+ "async": False
140
+ },
141
+ "query_wandb_tool": {
142
+ "function": query_paginated_wandb_gql,
143
+ "description": QUERY_WANDB_GQL_TOOL_DESCRIPTION,
144
+ "async": False
145
+ },
146
+ "create_wandb_report_tool": {
147
+ "function": create_report,
148
+ "description": CREATE_WANDB_REPORT_TOOL_DESCRIPTION,
149
+ "async": False
150
+ },
151
+ "query_wandb_entity_projects": {
152
+ "function": list_entity_projects,
153
+ "description": LIST_ENTITY_PROJECTS_TOOL_DESCRIPTION,
154
+ "async": False
155
+ },
156
+ "query_wandb_support_bot": {
157
+ "function": query_wandbot_api,
158
+ "description": WANDBOT_TOOL_DESCRIPTION,
159
+ "async": False
160
+ }
161
+ }
162
+
163
+ mcp_initialized = True
164
+ logger.info(f"MCP server initialized with {len(mcp_tools)} tools")
165
+
166
+ except Exception as e:
167
+ logger.error(f"Error initializing MCP server: {e}")
168
+ mcp_initialized = False
169
+
170
  @app.get("/", response_class=HTMLResponse)
171
  async def index():
172
  """Serve the landing page."""
 
175
  @app.get("/health")
176
  async def health():
177
  """Health check endpoint."""
 
178
  return {
179
  "status": "healthy",
180
  "service": "wandb-mcp-server",
181
+ "wandb_configured": wandb_configured,
182
+ "mcp_initialized": mcp_initialized,
183
+ "tools_count": len(mcp_tools)
184
  }
185
 
186
+ # MCP Streamable HTTP Implementation
187
+ @app.post("/mcp")
188
+ async def handle_mcp_post(request: Request):
189
+ """
190
+ Handle MCP POST requests following the streamable HTTP transport protocol.
191
+ """
192
+ if not mcp_initialized:
193
+ return JSONResponse(
194
+ status_code=503,
195
+ content={
196
+ "jsonrpc": "2.0",
197
+ "error": {
198
+ "code": -32603,
199
+ "message": "MCP server not initialized"
200
+ }
201
+ }
202
+ )
 
 
 
 
203
 
204
  try:
205
+ body = await request.json()
206
+ method = body.get("method", "")
207
+ params = body.get("params", {})
208
+ request_id = body.get("id")
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ # Handle different MCP methods
211
+ if method == "initialize":
212
+ return {
213
+ "jsonrpc": "2.0",
214
+ "id": request_id,
215
+ "result": {
216
+ "protocolVersion": "0.1.0",
217
+ "capabilities": {
218
+ "tools": {"listChanged": True},
219
+ "prompts": {"listChanged": False},
220
+ "resources": {"listChanged": False}
221
+ },
222
+ "serverInfo": {
223
+ "name": "wandb-mcp-server",
224
+ "version": "0.1.0"
225
+ }
226
+ }
227
+ }
228
 
229
+ elif method == "tools/list":
230
+ tools_list = []
231
+ for tool_name, tool_info in mcp_tools.items():
232
+ # Extract a shorter description (first line)
233
+ desc_lines = tool_info["description"].split('\n')
234
+ short_desc = desc_lines[0] if desc_lines else f"W&B tool: {tool_name}"
235
+
236
+ tools_list.append({
237
+ "name": tool_name,
238
+ "description": short_desc,
239
+ "inputSchema": {
240
+ "type": "object",
241
+ "properties": {},
242
+ "required": []
243
+ }
244
+ })
245
+
246
+ return {
247
+ "jsonrpc": "2.0",
248
+ "id": request_id,
249
+ "result": {"tools": tools_list}
250
+ }
251
 
252
+ elif method == "tools/call":
253
+ tool_name = params.get("name")
254
+ tool_args = params.get("arguments", {})
 
255
 
256
+ if tool_name not in mcp_tools:
257
+ return {
258
+ "jsonrpc": "2.0",
259
+ "id": request_id,
260
+ "error": {
261
+ "code": -32601,
262
+ "message": f"Tool not found: {tool_name}"
263
+ }
264
+ }
265
 
266
+ try:
267
+ tool_info = mcp_tools[tool_name]
268
+ tool_function = tool_info["function"]
269
+
270
+ # Execute the tool
271
+ if tool_info["async"]:
272
+ result = await tool_function(**tool_args)
273
+ else:
274
+ result = tool_function(**tool_args)
275
+
276
+ # Format the result
277
+ if isinstance(result, str):
278
+ content_text = result
279
+ elif isinstance(result, dict):
280
+ content_text = json.dumps(result, indent=2)
281
+ else:
282
+ content_text = str(result)
283
+
284
+ return {
285
+ "jsonrpc": "2.0",
286
+ "id": request_id,
287
+ "result": {
288
+ "content": [
289
+ {
290
+ "type": "text",
291
+ "text": content_text
292
+ }
293
+ ]
294
+ }
295
+ }
296
+
297
+ except Exception as e:
298
+ logger.error(f"Error executing tool {tool_name}: {e}")
299
+ return {
300
+ "jsonrpc": "2.0",
301
+ "id": request_id,
302
+ "error": {
303
+ "code": -32603,
304
+ "message": f"Tool execution error: {str(e)}"
305
+ }
306
+ }
307
 
308
+ else:
309
+ return {
310
+ "jsonrpc": "2.0",
311
+ "id": request_id,
312
+ "error": {
313
+ "code": -32601,
314
+ "message": f"Method not found: {method}"
315
+ }
316
+ }
317
+
318
+ except Exception as e:
319
+ logger.error(f"Error handling MCP request: {e}")
320
+ return JSONResponse(
321
+ status_code=500,
322
+ content={
323
+ "jsonrpc": "2.0",
324
+ "error": {
325
+ "code": -32603,
326
+ "message": f"Internal error: {str(e)}"
327
+ }
328
+ }
329
+ )
330
+
331
+ @app.get("/mcp")
332
+ async def handle_mcp_sse(request: Request):
333
+ """
334
+ Handle MCP GET requests for SSE (Server-Sent Events) streaming.
335
+ This enables server-initiated messages and long-lived connections.
336
+ """
337
+ if not mcp_initialized:
338
+ return JSONResponse(
339
+ status_code=503,
340
+ content={"error": "MCP server not initialized"}
341
+ )
342
 
343
+ async def event_stream():
344
+ """Generate server-sent events for MCP."""
345
+ try:
346
+ # Send initial connection confirmation
347
+ yield f"data: {json.dumps({'jsonrpc': '2.0', 'method': 'connection/ready', 'params': {'status': 'connected', 'timestamp': datetime.utcnow().isoformat()}})}\n\n"
348
+
349
+ # Keep connection alive with periodic heartbeats
350
+ while True:
351
+ await asyncio.sleep(30)
352
+ yield f"data: {json.dumps({'jsonrpc': '2.0', 'method': 'ping', 'params': {'timestamp': datetime.utcnow().isoformat()}})}\n\n"
353
+
354
+ except asyncio.CancelledError:
355
+ logger.info("SSE connection closed")
356
+ raise
357
+ except Exception as e:
358
+ logger.error(f"Error in SSE stream: {e}")
359
+ yield f"data: {json.dumps({'jsonrpc': '2.0', 'method': 'error', 'params': {'message': str(e)}})}\n\n"
360
 
361
+ return StreamingResponse(
362
+ event_stream(),
363
+ media_type="text/event-stream",
364
+ headers={
365
+ "Cache-Control": "no-cache",
366
+ "Connection": "keep-alive",
367
+ "X-Accel-Buffering": "no",
368
+ "Access-Control-Allow-Origin": "*"
369
+ }
370
+ )
371
+
372
+ # Additional MCP endpoints for better compatibility
373
+ @app.options("/mcp")
374
+ async def handle_mcp_options():
375
+ """Handle OPTIONS requests for CORS preflight."""
376
+ return Response(
377
+ status_code=200,
378
+ headers={
379
+ "Access-Control-Allow-Origin": "*",
380
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
381
+ "Access-Control-Allow-Headers": "Content-Type, Accept",
382
+ "Access-Control-Max-Age": "3600"
383
+ }
384
+ )
385
 
386
  def main():
387
  """Main entry point for HuggingFace Spaces."""
 
391
  logger.info(f"Starting server on {host}:{port}")
392
  logger.info("Landing page: /")
393
  logger.info("Health check: /health")
394
+ logger.info("MCP endpoint (Streamable HTTP): /mcp")
395
 
396
  uvicorn.run(
397
  app,