forgejo-mcp/demos/multi-tenant-http.md
Byte Flavour 4969ee5881
feat: robust stateless auth with security fixes and improved tests
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.
2026-05-24 13:44:48 +02:00

3.3 KiB

Demo: Multi-tenant HTTP Mode

2026-05-24T00:00:00Z by Showboat 0.6.1

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:

make build

Starting the server

Start the server without a global token (or with a fallback token):

./forgejo-mcp --transport http --url https://codeberg.org
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):

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:

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'
"user-a"

3. Request as User B (same session)

Pass User B's token in the same session:

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'
"user-b"

4. Request with invalid token

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": {}
    }
  }'
{"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).