This commit addresses the review feedback on PR #138: - Fixed a privilege escalation vulnerability where failed ephemeral client creation would silently fall back to the global singleton token. - Refactored forgejo.Client(ctx) to return (*forgejo.Client, error). - Updated all 100+ tool handlers to properly handle client initialization errors. - Improved token extraction to handle schemes (token/bearer) case-insensitively. - Enhanced Unit Tests (pkg/forgejo/forgejo_test.go) to verify that different concurrent requests with different tokens correctly use their respective tokens in the Authorization header. - Updated README and demos to reflect case-insensitivity support.
135 lines
3.3 KiB
Markdown
135 lines
3.3 KiB
Markdown
# Demo: Multi-tenant HTTP Mode
|
|
|
|
*2026-05-24T00:00:00Z by Showboat 0.6.1*
|
|
<!-- showboat-id: stateless-http-auth-demo-2026 -->
|
|
|
|
## What this feature does
|
|
|
|
Enables a single `forgejo-mcp` instance to serve multiple users or agents by extracting the Forgejo API token from the `Authorization` header of each individual MCP request. This makes the server completely stateless regarding authentication when using HTTP or SSE transports.
|
|
|
|
## Setup
|
|
|
|
Build the latest version:
|
|
|
|
```bash
|
|
make build
|
|
```
|
|
|
|
## Starting the server
|
|
|
|
Start the server without a global token (or with a fallback token):
|
|
|
|
```bash
|
|
./forgejo-mcp --transport http --url https://codeberg.org
|
|
```
|
|
|
|
```output
|
|
Starting Forgejo MCP Server ...
|
|
Starting MCP streamable HTTP server {"port": 8080}
|
|
MCP streamable HTTP server ready for connections {"port": 8080, "endpoint": "http://localhost:8080"}
|
|
```
|
|
|
|
## Multi-tenant usage with curl
|
|
|
|
In a second terminal, we can now make requests using different identities.
|
|
|
|
### 1. Initialize session
|
|
|
|
First, we need a session ID (standard for Streamable HTTP):
|
|
|
|
```bash
|
|
INIT_RESP=$(curl -s -i -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2025-03-26",
|
|
"capabilities": {},
|
|
"clientInfo": { "name": "multi-tenant-demo", "version": "1.0" }
|
|
}
|
|
}')
|
|
|
|
SESSION_ID=$(echo "$INIT_RESP" | grep -i "mcp-session-id" | awk '{print $2}' | tr -d '\r')
|
|
echo "Session ID: $SESSION_ID"
|
|
```
|
|
|
|
### 2. Request as User A
|
|
|
|
Pass User A's token in the `Authorization` header:
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-H "Mcp-Session-Id: $SESSION_ID" \
|
|
-H "Authorization: token <TOKEN_A>" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "get_my_user_info",
|
|
"arguments": {}
|
|
}
|
|
}' | jq '.result.content[0].text | fromjson | .login'
|
|
```
|
|
|
|
```output
|
|
"user-a"
|
|
```
|
|
|
|
### 3. Request as User B (same session)
|
|
|
|
Pass User B's token in the same session:
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-H "Mcp-Session-Id: $SESSION_ID" \
|
|
-H "Authorization: token <TOKEN_B>" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": 3,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "get_my_user_info",
|
|
"arguments": {}
|
|
}
|
|
}' | jq '.result.content[0].text | fromjson | .login'
|
|
```
|
|
|
|
```output
|
|
"user-b"
|
|
```
|
|
|
|
### 4. Request with invalid token
|
|
|
|
```bash
|
|
curl -s -X POST http://localhost:8080/mcp \
|
|
-H "Content-Type: application/json" \
|
|
-H "Mcp-Session-Id: $SESSION_ID" \
|
|
-H "Authorization: token invalid_token" \
|
|
-d '{
|
|
"jsonrpc": "2.0",
|
|
"id": 4,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "get_my_user_info",
|
|
"arguments": {}
|
|
}
|
|
}'
|
|
```
|
|
|
|
```output
|
|
{"jsonrpc":"2.0","id":4,"error":{"code":-32603,"message":"get user info err: token is required"}}
|
|
```
|
|
|
|
## Supported Header Formats
|
|
|
|
The server supports the following `Authorization` header formats:
|
|
- `Authorization: token <token>` (Forgejo/Gitea style)
|
|
- `Authorization: Bearer <token>` (Standard OAuth2 style)
|
|
- `Authorization: <token>` (Minimalist style)
|
|
|
|
Note: The schemes (`token` and `Bearer`) are matched case-insensitively (e.g., `bearer`, `Bearer`, `BEARER` all work).
|