# ADR-0005: Per-User Authentication for Hosted MCP Server
**Status:** Accepted
**Date:** 2026-01-22
**Decision Makers:** MCP Server Team
## Context
Jana Earth Data hosts the MCP server as a service. Individual users connect from their local Cursor or Claude installations. Each user has their own username/password credentials assigned by Jana Earth Data.
### Key Constraints Discovered
1. **MCP clients don't support custom headers for SSE connections** — Cursor, Claude Desktop, and Claude Code cannot pass `Authorization` headers to SSE endpoints. This is a confirmed limitation with open feature requests.
2. **SSE transport is deprecated** — MCP deprecated HTTP+SSE in March 2025 in favor of Streamable HTTP, though SSE still works.
3. **Working authentication methods:**
- Token in URL query parameter: `/sse?token=xxx` (works with all clients)
- mcp-remote proxy: local process adds headers (requires Node.js)
## Decision
Implement **dual authentication support**:
1. **Primary:** Token in query parameter (simplest, works everywhere)
2. **Secondary:** Token in Authorization header (for mcp-remote and future clients)
Header-based auth takes precedence when both are provided.
## Implementation
### Token Extraction (Priority Order)
```python
def _extract_auth_token(scope: Scope) -> tuple[str | None, str]:
# 1. Try Authorization header (preferred, more secure)
auth_header = headers.get(b"authorization", b"").decode()
if auth_header.startswith("Token "):
return auth_header[6:], "header"
if auth_header.startswith("Bearer "):
return auth_header[7:], "header"
# 2. Fallback: query parameter (works with all clients)
params = dict(parse_qsl(query_string))
if token := params.get("token"):
return token, "query_param"
return None, "none"
```
### User Configuration Options
**Option A: Direct URL (Recommended)**
```json
{
"mcpServers": {
"jana": {
"url": "https://mcp.janaearth.com/sse?token=USER_TOKEN"
}
}
}
```
**Option B: mcp-remote Proxy**
```json
{
"mcpServers": {
"jana": {
"command": "npx",
"args": ["mcp-remote", "https://mcp.janaearth.com/sse",
"--header", "Authorization:${JANA_TOKEN}"],
"env": {"JANA_TOKEN": "Token USER_TOKEN"}
}
}
}
```
## Security Considerations
### Query Parameter Token Risks
| Risk | Mitigation |
|------|------------|
| Token in URL | HTTPS encrypts entire URL in transit |
| Server logs | Don't log query parameters |
| Browser history | MCP clients are apps, not browsers |
### Additional Protections
- Short-lived tokens with rotation capability
- Per-user rate limiting
- Audit logging of all authenticated requests
- HTTPS-only in production
## Consequences
### Positive
- Works with all current MCP clients without user-side dependencies
- Simple user experience (just add token to URL)
- Maintains header support for future improvements
- Per-user access control and audit trails
### Negative
- Token visible in URL (mitigated by HTTPS)
- Not using OAuth 2.1 (MCP specification recommendation)
### Future Work
1. Add Streamable HTTP transport when client support matures
2. Implement OAuth 2.1 for enterprise customers
3. Support token refresh/rotation API
## Alternatives Considered
### 1. Header-Only Authentication
**Rejected:** No current MCP client supports sending custom headers to SSE endpoints.
### 2. OAuth 2.1
**Deferred:** Adds significant complexity. Current clients have buggy/incomplete OAuth support. Will revisit for Phase 2.
### 3. Require mcp-remote for All Users
**Rejected:** Adds Node.js dependency and complexity for all users. Only recommend for users with strict security requirements.
## References
- [MCP Transport Architecture](../MCP_TRANSPORT_ARCHITECTURE.md)
- [User Manual](../../USER_MANUAL.md)
- [Cursor Forum - SSE Header Support Request](https://forum.cursor.com/t/api-key-for-sse-mcp-servers/63300)
- [mcp-remote npm package](https://www.npmjs.com/package/mcp-remote)