NiWaRe commited on
Commit
e2aaee8
·
1 Parent(s): a2dc155

clean up and oauth experiments

Browse files
AUTH_README.md ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # W&B MCP Server Authentication
2
+
3
+ The W&B MCP Server uses **Bearer token authentication** with W&B API keys for secure access to your Weights & Biases data.
4
+
5
+ ## How It Works
6
+
7
+ ### API Key Authentication
8
+
9
+ The server uses standard HTTP Bearer token authentication where your W&B API key serves as the Bearer token:
10
+
11
+ ```http
12
+ Authorization: Bearer YOUR_WANDB_API_KEY
13
+ ```
14
+
15
+ **Key Features:**
16
+ - Each client provides their own W&B API key
17
+ - Server uses the client's key for all W&B operations
18
+ - Perfect isolation between users
19
+ - No server-side API key needed for HTTP transport
20
+
21
+ ### Getting Your W&B API Key
22
+
23
+ 1. Go to [https://wandb.ai/authorize](https://wandb.ai/authorize)
24
+ 2. Log in with your W&B account
25
+ 3. Copy your API key (exactly 40 characters, no spaces)
26
+ 4. Use it as the Bearer token in your MCP client
27
+
28
+ **Important**: W&B API keys must be exactly 40 alphanumeric characters. Make sure you copy the entire key without any extra spaces or line breaks.
29
+
30
+ ## Client Configuration
31
+
32
+ ### Mistral LeChat
33
+
34
+ In Mistral LeChat, add a Custom MCP Connector:
35
+
36
+ 1. **Server URL**: `https://your-space.hf.space/mcp`
37
+ 2. **Authentication**: Choose "HTTP Bearer Token"
38
+ 3. **Token**: Enter your W&B API key
39
+
40
+ ### Claude Desktop / Cursor
41
+
42
+ Configure in your MCP settings:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "wandb": {
48
+ "transport": "http",
49
+ "url": "http://localhost:8080/mcp",
50
+ "headers": {
51
+ "Authorization": "Bearer YOUR_WANDB_API_KEY",
52
+ "Accept": "application/json, text/event-stream"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ **Important Headers:**
60
+ - `Authorization`: Your W&B API key as Bearer token
61
+ - `Accept`: Must include both `application/json` and `text/event-stream` for MCP Streamable HTTP
62
+
63
+ ### Python Client Example
64
+
65
+ ```python
66
+ import requests
67
+
68
+ # Initialize MCP session
69
+ response = requests.post(
70
+ "http://localhost:8080/mcp",
71
+ headers={
72
+ "Authorization": "Bearer YOUR_WANDB_API_KEY",
73
+ "Accept": "application/json, text/event-stream",
74
+ "Content-Type": "application/json"
75
+ },
76
+ json={
77
+ "jsonrpc": "2.0",
78
+ "method": "initialize",
79
+ "params": {},
80
+ "id": 1
81
+ }
82
+ )
83
+ ```
84
+
85
+ ## Security Considerations
86
+
87
+ ### Non-Expiring API Keys
88
+
89
+ W&B API keys don't expire by default, similar to GitHub Personal Access Tokens or OpenAI API keys. This is a design choice by W&B for developer convenience.
90
+
91
+ **Best Practices:**
92
+ - Rotate keys regularly (quarterly recommended)
93
+ - Use separate keys for different services
94
+ - Monitor usage at [wandb.ai/settings](https://wandb.ai/settings)
95
+ - Revoke compromised keys immediately
96
+ - Never commit keys to version control
97
+
98
+ ### Multi-User Deployment
99
+
100
+ For HuggingFace Spaces or shared deployments:
101
+ - Server requires no API key configuration
102
+ - Each user provides their own key
103
+ - Keys are used transiently per request
104
+ - No keys are stored or logged
105
+
106
+ ## OAuth: Why We Can't Support Full OAuth 2.0
107
+
108
+ ### What We Tried
109
+
110
+ We attempted to implement OAuth 2.0 support to provide a seamless authentication experience, especially for clients like Mistral LeChat that expect OAuth for custom connectors. This included:
111
+
112
+ 1. **OAuth Discovery Endpoints**: `/.well-known/oauth-authorization-server`
113
+ 2. **Authorization Flow**: Redirect to W&B's Auth0 login
114
+ 3. **Token Exchange**: Accept W&B API keys as "access tokens"
115
+ 4. **Device Flow**: Guide users to get their API key
116
+
117
+ ### Why It Doesn't Work
118
+
119
+ **Fundamental Limitations:**
120
+
121
+ 1. **W&B Doesn't Provide OAuth for Third Parties**
122
+ - W&B uses Auth0 internally but doesn't allow third-party OAuth client registration
123
+ - No way to register our MCP server as an OAuth application
124
+ - Can't receive authorization codes or callbacks from W&B
125
+
126
+ 2. **API Keys Are Not OAuth Tokens**
127
+ - W&B provides permanent API keys, not temporary OAuth tokens
128
+ - No refresh mechanism, no expiration, no scopes
129
+ - Keys are managed through W&B's web interface, not OAuth flows
130
+
131
+ 3. **Cross-Domain Issues**
132
+ - OAuth requires the authorization server and resource server to cooperate
133
+ - W&B's Auth0 instance (`wandb.auth0.com`) doesn't know about our server
134
+ - Can't validate tokens or handle callbacks
135
+
136
+ ### What Would Be Needed for Full OAuth
137
+
138
+ For proper OAuth 2.0 support, W&B would need to:
139
+
140
+ 1. **Allow OAuth Client Registration**
141
+ - Let developers register OAuth applications
142
+ - Provide client ID and secret
143
+ - Support redirect URIs for callbacks
144
+
145
+ 2. **Issue Real OAuth Tokens**
146
+ - Temporary access tokens (e.g., 1-hour expiry)
147
+ - Refresh tokens for obtaining new access tokens
148
+ - Scoped permissions (read-only, write, admin)
149
+
150
+ 3. **Provide Token Validation**
151
+ - Introspection endpoint for validating tokens
152
+ - Revocation endpoint for invalidating tokens
153
+ - JWKS endpoint for JWT validation
154
+
155
+ ### Current Solution
156
+
157
+ Given these limitations, we use W&B API keys directly as Bearer tokens. This approach:
158
+ - ✅ Works with all W&B functionality
159
+ - ✅ Compatible with MCP specification
160
+ - ✅ Simple and reliable
161
+ - ✅ Follows industry patterns (GitHub, OpenAI)
162
+ - ❌ Requires manual key management
163
+ - ❌ No automatic token refresh
164
+
165
+ ## Troubleshooting
166
+
167
+ ### Common Issues
168
+
169
+ #### "Authorization required" (401)
170
+ - Ensure Bearer token is included in header
171
+ - Check API key is valid at [wandb.ai/settings](https://wandb.ai/settings)
172
+
173
+ #### "Not Acceptable" (406)
174
+ - Include `Accept: application/json, text/event-stream` header
175
+ - Required for MCP Streamable HTTP transport
176
+
177
+ #### "Missing session ID" (400)
178
+ - Call `initialize` method first
179
+ - Include session ID from response in subsequent requests
180
+
181
+ #### "Invalid API key format"
182
+ - W&B API keys are ~40 alphanumeric characters
183
+ - Get a valid key from [wandb.ai/authorize](https://wandb.ai/authorize)
184
+
185
+ ### Testing Authentication
186
+
187
+ ```bash
188
+ # Test with curl
189
+ curl -X POST http://localhost:8080/mcp \
190
+ -H "Authorization: Bearer YOUR_WANDB_API_KEY" \
191
+ -H "Accept: application/json, text/event-stream" \
192
+ -H "Content-Type: application/json" \
193
+ -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'
194
+ ```
195
+
196
+ ## Development Mode
197
+
198
+ For local development only, you can disable authentication:
199
+
200
+ ```bash
201
+ export MCP_AUTH_DISABLED=true
202
+ ```
203
+
204
+ ⚠️ **Warning**: Never use this in production or on public servers!
HUGGINGFACE_DEPLOYMENT.md CHANGED
@@ -37,18 +37,18 @@ The application runs as a FastAPI server on port 7860 (HF Spaces default) with:
37
 
38
  ## Environment Variables
39
 
40
- Required in HF Spaces settings:
41
- ```
42
- WANDB_API_KEY=your_api_key_here
43
- ```
44
 
45
- Optional:
46
  ```
47
- WANDB_ENTITY=your_wandb_entity
48
- MCP_LOGS_WANDB_PROJECT=wandb-mcp-logs
 
49
  WEAVE_DISABLED=false # Set to enable Weave tracing
50
  ```
51
 
 
 
52
  ## Deployment Steps
53
 
54
  1. **Create a new Space on Hugging Face**
@@ -151,6 +151,153 @@ For MCP clients that support streamable HTTP:
151
  }
152
  ```
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  ## Differences from Standard Deployment
155
 
156
  | Feature | Standard | HF Spaces |
 
37
 
38
  ## Environment Variables
39
 
40
+ No environment variables are required! The server works without any configuration.
 
 
 
41
 
42
+ Optional (for server-side logging only):
43
  ```
44
+ WANDB_API_KEY=your_api_key_here # Only if you want server-side Weave tracing
45
+ WANDB_ENTITY=your_wandb_entity # For server-side Weave tracing
46
+ MCP_LOGS_WANDB_PROJECT=wandb-mcp-logs # Project for server logs
47
  WEAVE_DISABLED=false # Set to enable Weave tracing
48
  ```
49
 
50
+ **Note**: For normal operation, users provide their own W&B API keys as Bearer tokens. No server configuration needed.
51
+
52
  ## Deployment Steps
53
 
54
  1. **Create a new Space on Hugging Face**
 
151
  }
152
  ```
153
 
154
+ ## MCP Architecture & Key Learnings
155
+
156
+ ### Understanding MCP and FastMCP
157
+
158
+ The Model Context Protocol (MCP) is a protocol for communication between AI assistants and external tools/services. Through our experimentation, we discovered several important aspects:
159
+
160
+ #### 1. FastMCP Framework
161
+ - **FastMCP** is a Python framework that simplifies MCP server implementation
162
+ - It provides decorators (`@mcp.tool()`) for easy tool registration
163
+ - Internally uses Starlette for HTTP handling
164
+ - Supports multiple transports: stdio, SSE, and streamable HTTP
165
+
166
+ #### 2. Streamable HTTP Transport
167
+ The streamable HTTP transport (introduced in [MCP PR #206](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/206)) is the modern approach for remote MCP:
168
+
169
+ - **Single endpoint** (`/mcp`) handles all communication
170
+ - **Dual mode operation**:
171
+ - Regular POST requests for stateless operations
172
+ - SSE (Server-Sent Events) upgrade for streaming responses
173
+ - **Key advantages**:
174
+ - Stateless servers possible (no persistent connections required)
175
+ - Better infrastructure compatibility ("just HTTP")
176
+ - Supports both request-response and streaming patterns
177
+
178
+ #### 3. Implementation Patterns
179
+
180
+ ##### The HuggingFace Pattern
181
+ Based on the [reference implementation](https://huggingface.co/spaces/Jofthomas/Multiple_mcp_fastapi_template), the correct pattern is:
182
+
183
+ ```python
184
+ # Create MCP server
185
+ mcp = FastMCP("server-name")
186
+
187
+ # Register tools
188
+ @mcp.tool()
189
+ def my_tool(): ...
190
+
191
+ # Get streamable HTTP app (returns Starlette app)
192
+ mcp_app = mcp.streamable_http_app()
193
+
194
+ # Mount in FastAPI
195
+ app.mount("/", mcp_app) # Note: mount at root, not at /mcp
196
+ ```
197
+
198
+ ##### Why Mount at Root?
199
+ - `streamable_http_app()` creates internal routes at `/mcp`
200
+ - Mounting at `/mcp` would create `/mcp/mcp` (double path)
201
+ - Mounting at root gives us the clean `/mcp` endpoint
202
+
203
+ #### 4. Session Management
204
+ - FastMCP includes a `session_manager` for handling stateful operations
205
+ - Use lifespan context manager to properly initialize/cleanup:
206
+ ```python
207
+ async with mcp.session_manager.run():
208
+ yield
209
+ ```
210
+
211
+ #### 5. Response Format
212
+ - MCP uses **Server-Sent Events (SSE)** for responses
213
+ - Responses are prefixed with `event: message` and `data: `
214
+ - JSON-RPC format for the actual message content
215
+ - Example response:
216
+ ```
217
+ event: message
218
+ data: {"jsonrpc":"2.0","id":1,"result":{...}}
219
+ ```
220
+
221
+ ### Critical Implementation Details
222
+
223
+ #### 1. Required Headers
224
+ Clients MUST send:
225
+ - `Content-Type: application/json`
226
+ - `Accept: application/json, text/event-stream`
227
+
228
+ Without the correct Accept header, the server returns a "Not Acceptable" error.
229
+
230
+ #### 2. Lazy Loading Pattern
231
+ To avoid initialization issues (e.g., API keys required at import time):
232
+ ```python
233
+ # Instead of this:
234
+ _service = Service() # Fails if no API key
235
+
236
+ # Use lazy loading:
237
+ _service = None
238
+ def get_service():
239
+ global _service
240
+ if _service is None:
241
+ _service = Service()
242
+ return _service
243
+ ```
244
+
245
+ #### 3. Environment Setup for HF Spaces
246
+ Critical for avoiding permission errors:
247
+ ```python
248
+ os.environ["WANDB_CACHE_DIR"] = "/tmp/.wandb_cache"
249
+ os.environ["HOME"] = "/tmp"
250
+ ```
251
+
252
+ ### Common Pitfalls & Solutions
253
+
254
+ | Issue | Symptom | Solution |
255
+ |-------|---------|----------|
256
+ | Double path (`/mcp/mcp`) | 404 errors on `/mcp` | Mount streamable_http_app() at root (`/`) |
257
+ | Missing Accept header | "Not Acceptable" error | Include `Accept: application/json, text/event-stream` |
258
+ | Import-time API key errors | Server fails to start | Use lazy loading pattern |
259
+ | Permission errors in HF Spaces | `mkdir /.cache: permission denied` | Set cache dirs to `/tmp` |
260
+ | Can't access MCP methods | Methods not exposed | Use FastMCP's built-in decorators and methods |
261
+
262
+ ### Testing Strategy
263
+
264
+ 1. **Local Testing**: Always test with correct headers
265
+ 2. **Check Routes**: Verify mounting creates `/mcp` endpoint
266
+ 3. **Test Initialize First**: This method doesn't require session state
267
+ 4. **SSE Response Parsing**: Remember responses are SSE formatted, not plain JSON
268
+
269
+ ### Evolution of Our Implementation
270
+
271
+ Our journey to the correct implementation went through several iterations:
272
+
273
+ #### Attempt 1: Direct Protocol Implementation
274
+ - **Approach**: Implement MCP protocol directly in FastAPI
275
+ - **Issue**: Reinventing the wheel, not using FastMCP's built-in capabilities
276
+ - **Learning**: FastMCP already handles the protocol complexity
277
+
278
+ #### Attempt 2: Trying to Extract FastMCP's Internal App
279
+ - **Approach**: Access FastMCP's internal FastAPI app via attributes
280
+ - **Issue**: FastMCP doesn't expose its app in an accessible way
281
+ - **Learning**: Need to use FastMCP's intended methods
282
+
283
+ #### Attempt 3: Using http_app() Method
284
+ - **Approach**: Try various methods like `http_app()`, `asgi_app()`, etc.
285
+ - **Issue**: These methods either don't exist or don't work as expected
286
+ - **Learning**: Documentation and examples are crucial
287
+
288
+ #### Attempt 4: The Correct Pattern
289
+ - **Approach**: Use `streamable_http_app()` following HuggingFace example
290
+ - **Success**: Works perfectly when mounted at root
291
+ - **Key Insight**: The example pattern exists for a reason - follow it!
292
+
293
+ ### Key Takeaways
294
+
295
+ 1. **Follow Existing Examples**: The HuggingFace example was the key to success
296
+ 2. **Understand the Protocol**: MCP uses SSE for good reasons (streaming, stateless option)
297
+ 3. **Lazy Loading is Critical**: Avoid initialization-time dependencies
298
+ 4. **Environment Matters**: HF Spaces has specific constraints (ports, permissions)
299
+ 5. **Test Incrementally**: Start with basic endpoints before complex operations
300
+
301
  ## Differences from Standard Deployment
302
 
303
  | Feature | Standard | HF Spaces |
app.py CHANGED
@@ -22,7 +22,7 @@ os.environ["HOME"] = "/tmp"
22
  os.environ["WANDB_SILENT"] = "True"
23
  os.environ["WEAVE_SILENT"] = "True"
24
 
25
- from fastapi import FastAPI
26
  from fastapi.responses import HTMLResponse, JSONResponse
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from mcp.server.fastmcp import FastMCP
@@ -38,11 +38,7 @@ from wandb_mcp_server.server import (
38
  )
39
 
40
  # Import authentication
41
- from wandb_mcp_server.auth import (
42
- mcp_auth_middleware,
43
- create_resource_metadata_response,
44
- MCPAuthConfig
45
- )
46
 
47
  # Configure logging
48
  logging.basicConfig(
@@ -124,11 +120,8 @@ async def index():
124
  """Serve the landing page."""
125
  return INDEX_HTML_CONTENT
126
 
127
- @app.get("/.well-known/oauth-protected-resource")
128
- async def resource_metadata():
129
- """OAuth 2.0 Protected Resource Metadata endpoint (RFC 9728)."""
130
- config = MCPAuthConfig()
131
- return JSONResponse(create_resource_metadata_response(config))
132
 
133
  @app.get("/health")
134
  async def health():
 
22
  os.environ["WANDB_SILENT"] = "True"
23
  os.environ["WEAVE_SILENT"] = "True"
24
 
25
+ from fastapi import FastAPI, Request
26
  from fastapi.responses import HTMLResponse, JSONResponse
27
  from fastapi.middleware.cors import CORSMiddleware
28
  from mcp.server.fastmcp import FastMCP
 
38
  )
39
 
40
  # Import authentication
41
+ from wandb_mcp_server.auth import mcp_auth_middleware
 
 
 
 
42
 
43
  # Configure logging
44
  logging.basicConfig(
 
120
  """Serve the landing page."""
121
  return INDEX_HTML_CONTENT
122
 
123
+ # Removed OAuth endpoints - only API key authentication is supported
124
+ # See AUTH_README.md for details on why full OAuth isn't feasible
 
 
 
125
 
126
  @app.get("/health")
127
  async def health():
auth.example.env ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # W&B MCP Server Authentication Configuration Example
2
+ # Copy this file to .env and add your values
3
+
4
+ # ================================
5
+ # API KEY AUTHENTICATION
6
+ # ================================
7
+
8
+ # For STDIO transport (required):
9
+ # Get your key from: https://wandb.ai/authorize
10
+ WANDB_API_KEY=your_wandb_api_key_here
11
+
12
+ # For HTTP transport (optional):
13
+ # Users provide their own API keys as Bearer tokens
14
+ # No server configuration needed
15
+
16
+ # ================================
17
+ # OPTIONAL SETTINGS
18
+ # ================================
19
+
20
+ # W&B Entity (team or username) for server-side logging
21
+ # WANDB_ENTITY=your_team_or_username
22
+
23
+ # Project for MCP operation traces (if Weave tracing enabled)
24
+ # MCP_LOGS_WANDB_PROJECT=wandb-mcp-logs
25
+
26
+ # Disable Weave tracing (set to true to disable)
27
+ # WEAVE_DISABLED=true
28
+
29
+ # ================================
30
+ # DEVELOPMENT ONLY
31
+ # ================================
32
+
33
+ # Disable authentication entirely (NEVER use in production!)
34
+ # MCP_AUTH_DISABLED=true
src/wandb_mcp_server/auth.py CHANGED
@@ -29,30 +29,29 @@ class MCPAuthConfig:
29
  For HTTP transport: Accepts any W&B API key as a Bearer token.
30
  The server uses the client's token for all W&B operations.
31
  """
32
-
33
- def __init__(self):
34
- self.resource_metadata_url = os.environ.get(
35
- "MCP_RESOURCE_METADATA_URL",
36
- "/.well-known/oauth-protected-resource"
37
- )
38
- # Point to W&B's Auth0 instance for reference
39
- self.authorization_server = os.environ.get(
40
- "MCP_AUTH_SERVER",
41
- "https://wandb.auth0.com"
42
- )
43
 
44
 
45
  def is_valid_wandb_api_key(token: str) -> bool:
46
  """
47
  Check if a token looks like a valid W&B API key format.
48
- W&B API keys are typically 40 characters of alphanumeric + some special chars.
49
  """
50
- if not token or len(token) < 20 or len(token) > 100:
51
  return False
 
 
 
 
 
 
 
 
 
52
  # Basic validation - W&B keys contain alphanumeric and some special characters
53
- # This is a permissive check since W&B key format may vary
54
  if re.match(r'^[a-zA-Z0-9_\-\.]+$', token):
55
  return True
 
56
  return False
57
 
58
 
@@ -77,26 +76,24 @@ async def validate_bearer_token(
77
  status_code=status.HTTP_401_UNAUTHORIZED,
78
  detail="Authorization required - please provide your W&B API key as a Bearer token",
79
  headers={
80
- "WWW-Authenticate": f'Bearer realm="W&B MCP", '
81
- f'resource_metadata="{config.resource_metadata_url}"'
82
  }
83
  )
84
 
85
- token = credentials.credentials
86
 
87
  # Basic format validation
88
  if not is_valid_wandb_api_key(token):
89
  raise HTTPException(
90
  status_code=status.HTTP_401_UNAUTHORIZED,
91
- detail="Invalid W&B API key format. Get your key at: https://wandb.ai/authorize",
 
92
  headers={
93
- "WWW-Authenticate": f'Bearer realm="W&B MCP", '
94
- f'error="invalid_token", '
95
- f'resource_metadata="{config.resource_metadata_url}"'
96
  }
97
  )
98
 
99
- logger.debug("Bearer token validated successfully")
100
  return token
101
 
102
 
@@ -124,14 +121,19 @@ async def mcp_auth_middleware(request: Request, call_next):
124
  authorization = request.headers.get("Authorization", "")
125
  credentials = None
126
  if authorization.startswith("Bearer "):
 
 
127
  credentials = HTTPAuthorizationCredentials(
128
  scheme="Bearer",
129
- credentials=authorization[7:] # Remove "Bearer " prefix
130
  )
131
 
132
  # Validate and get the W&B API key
133
  wandb_api_key = await validate_bearer_token(credentials, config)
134
 
 
 
 
135
  # Store the API key in request state for W&B operations
136
  # The MCP tools should access this from the request context
137
  request.state.wandb_api_key = wandb_api_key
@@ -139,8 +141,16 @@ async def mcp_auth_middleware(request: Request, call_next):
139
  # For now, we'll set it in environment (in production, use contextvars)
140
  # Save the original value to restore later
141
  original_api_key = os.environ.get("WANDB_API_KEY")
 
 
142
  os.environ["WANDB_API_KEY"] = wandb_api_key
143
 
 
 
 
 
 
 
144
  try:
145
  # Continue processing
146
  response = await call_next(request)
@@ -166,23 +176,9 @@ async def mcp_auth_middleware(request: Request, call_next):
166
  status_code=status.HTTP_401_UNAUTHORIZED,
167
  content={"error": "Authentication failed"},
168
  headers={
169
- "WWW-Authenticate": f'Bearer realm="W&B MCP", '
170
- f'resource_metadata="{config.resource_metadata_url}"'
171
  }
172
  )
173
 
174
 
175
- def create_resource_metadata_response(config: MCPAuthConfig) -> Dict[str, Any]:
176
- """
177
- Create OAuth 2.0 Protected Resource Metadata response (RFC 9728).
178
-
179
- This tells MCP clients that we use W&B API keys as Bearer tokens.
180
- Points to W&B's Auth0 instance where users can get their API keys.
181
- """
182
- return {
183
- "resource": os.environ.get("MCP_SERVER_URL", "https://wandb-mcp-server.hf.space"),
184
- "authorization_servers": [config.authorization_server],
185
- "bearer_methods_supported": ["header"],
186
- "resource_documentation": "https://github.com/wandb/wandb-mcp-server",
187
- "authentication_note": "Use your W&B API key as a Bearer token. Get your key at https://wandb.ai/authorize",
188
- }
 
29
  For HTTP transport: Accepts any W&B API key as a Bearer token.
30
  The server uses the client's token for all W&B operations.
31
  """
32
+ pass # Simple config, no OAuth metadata needed
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  def is_valid_wandb_api_key(token: str) -> bool:
36
  """
37
  Check if a token looks like a valid W&B API key format.
38
+ W&B API keys are typically 40 characters but we'll be permissive.
39
  """
40
+ if not token:
41
  return False
42
+
43
+ # Strip any whitespace that might have been included
44
+ token = token.strip()
45
+
46
+ # Be permissive - accept keys between 20 and 100 characters
47
+ # The actual W&B API will validate the exact format
48
+ if len(token) < 20 or len(token) > 100:
49
+ return False
50
+
51
  # Basic validation - W&B keys contain alphanumeric and some special characters
 
52
  if re.match(r'^[a-zA-Z0-9_\-\.]+$', token):
53
  return True
54
+
55
  return False
56
 
57
 
 
76
  status_code=status.HTTP_401_UNAUTHORIZED,
77
  detail="Authorization required - please provide your W&B API key as a Bearer token",
78
  headers={
79
+ "WWW-Authenticate": 'Bearer realm="W&B MCP"'
 
80
  }
81
  )
82
 
83
+ token = credentials.credentials.strip() # Strip any whitespace
84
 
85
  # Basic format validation
86
  if not is_valid_wandb_api_key(token):
87
  raise HTTPException(
88
  status_code=status.HTTP_401_UNAUTHORIZED,
89
+ detail=f"Invalid W&B API key format. Got {len(token)} characters. "
90
+ f"Get your key at: https://wandb.ai/authorize",
91
  headers={
92
+ "WWW-Authenticate": 'Bearer realm="W&B MCP", error="invalid_token"'
 
 
93
  }
94
  )
95
 
96
+ logger.debug(f"Bearer token validated successfully (length: {len(token)})")
97
  return token
98
 
99
 
 
121
  authorization = request.headers.get("Authorization", "")
122
  credentials = None
123
  if authorization.startswith("Bearer "):
124
+ # Remove "Bearer " prefix and strip any whitespace
125
+ token = authorization[7:].strip()
126
  credentials = HTTPAuthorizationCredentials(
127
  scheme="Bearer",
128
+ credentials=token
129
  )
130
 
131
  # Validate and get the W&B API key
132
  wandb_api_key = await validate_bearer_token(credentials, config)
133
 
134
+ # Make sure the key is clean (no extra whitespace or encoding issues)
135
+ wandb_api_key = wandb_api_key.strip()
136
+
137
  # Store the API key in request state for W&B operations
138
  # The MCP tools should access this from the request context
139
  request.state.wandb_api_key = wandb_api_key
 
141
  # For now, we'll set it in environment (in production, use contextvars)
142
  # Save the original value to restore later
143
  original_api_key = os.environ.get("WANDB_API_KEY")
144
+
145
+ # Set the clean API key
146
  os.environ["WANDB_API_KEY"] = wandb_api_key
147
 
148
+ # Debug logging (remove in production)
149
+ logger.info(f"Auth middleware: Set WANDB_API_KEY with length={len(wandb_api_key)}, "
150
+ f"first_6={wandb_api_key[:6] if len(wandb_api_key) >= 6 else 'N/A'}..., "
151
+ f"last_4={wandb_api_key[-4:] if len(wandb_api_key) >= 4 else 'N/A'}, "
152
+ f"is_40_chars={len(wandb_api_key) == 40}")
153
+
154
  try:
155
  # Continue processing
156
  response = await call_next(request)
 
176
  status_code=status.HTTP_401_UNAUTHORIZED,
177
  content={"error": "Authentication failed"},
178
  headers={
179
+ "WWW-Authenticate": 'Bearer realm="W&B MCP"'
 
180
  }
181
  )
182
 
183
 
184
+ # OAuth-related functions removed - see AUTH_README.md for details
 
 
 
 
 
 
 
 
 
 
 
 
 
src/wandb_mcp_server/mcp_tools/count_traces.py CHANGED
@@ -180,6 +180,11 @@ def count_traces(
180
  if not api_key:
181
  logger.error("WANDB_API_KEY not found in environment variables.")
182
  raise ValueError("WANDB_API_KEY is required to query Weave traces count.")
 
 
 
 
 
183
 
184
  request_body: Dict[str, Any] = {"project_id": project_id}
185
  filter_payload: Dict[
@@ -296,6 +301,11 @@ def count_traces(
296
  if response.status_code != 200:
297
  error_msg = f"Error querying Weave trace count: {response.status_code} - {response.text}"
298
  logger.error(error_msg)
 
 
 
 
 
299
  # Log request body for easier debugging on error
300
  logger.debug(f"Failed request body: {json.dumps(request_body)}")
301
  raise Exception(error_msg)
 
180
  if not api_key:
181
  logger.error("WANDB_API_KEY not found in environment variables.")
182
  raise ValueError("WANDB_API_KEY is required to query Weave traces count.")
183
+
184
+ # Debug logging to diagnose API key issues
185
+ logger.debug(f"Using W&B API key: length={len(api_key)}, "
186
+ f"first_6={api_key[:6] if len(api_key) >= 6 else 'N/A'}..., "
187
+ f"last_4={api_key[-4:] if len(api_key) >= 4 else 'N/A'}")
188
 
189
  request_body: Dict[str, Any] = {"project_id": project_id}
190
  filter_payload: Dict[
 
301
  if response.status_code != 200:
302
  error_msg = f"Error querying Weave trace count: {response.status_code} - {response.text}"
303
  logger.error(error_msg)
304
+ # Log API key info for debugging
305
+ logger.error(f"API key info: length={len(api_key)}, is_40_chars={len(api_key) == 40}")
306
+ if "40 characters" in response.text:
307
+ logger.error(f"W&B requires exactly 40 character API keys. Current key has {len(api_key)} characters.")
308
+ logger.error(f"Key preview: {api_key[:8]}...{api_key[-4:] if len(api_key) >= 12 else ''}")
309
  # Log request body for easier debugging on error
310
  logger.debug(f"Failed request body: {json.dumps(request_body)}")
311
  raise Exception(error_msg)