Files
marcus d76c16d9a7 feat(auth): forward SONARQUBE_TOKEN to upstream as Bearer header
The upstream MCP container requires a SonarQube user token in the
Authorization header. Without one, every call returns 401.

- proxy: read SONARQUBE_TOKEN via sonarqube_token() at session-open
  time; raise TokenMissingError when unset/blank. upstream_session()
  attaches the token as "Authorization: Bearer <token>" via
  streamablehttp_client(headers=...).
- cli: fail fast in serve and check with a clear stderr message and
  exit 1 when the token is missing, before any network attempt. All
  exception text written to stderr passes through _redact() so an
  accidentally-leaked token from a third-party exception is replaced
  with [REDACTED] before display.
- The token is never stored on any object, never logged, and the
  TokenMissingError message contains no token material (it only
  describes how to generate one in SonarQube).
- Tests: header forwarding via mocked streamablehttp_client, missing-
  token exit code, redaction in CLI error paths, whitespace stripping
  on the token. Total: 25 tests.
- Docs: README/CLAUDE updated with the new env-var, Claude Desktop
  config snippet, and the security guarantees. CHANGELOG added.

Bumps version to 0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:42:51 +02:00

105 lines
4.3 KiB
Markdown

# CLAUDE.md
Entwicklungshinweise fuer `mcp-sonarqube-proxy`.
## Projekt
Stdio-MCP-Server, der beim Start eine Streamable-HTTP-Verbindung zum Upstream
SonarQube-MCP-Server oeffnet, dessen Tools 1:1 weiterreicht und Aufrufe an
diesen weiterleitet. Claude Desktop / Claude App spawnt diesen Prozess via stdio.
## Architektur
- `src/mcp_sonarqube_proxy/proxy.py` — eine `upstream_session()` (async context
manager) haelt die persistente `ClientSession` zum Upstream. `build_proxy_server()`
registriert zwei Handler auf einem `mcp.server.lowlevel.Server`, die Aufrufe
transparent weiterleiten.
- `src/mcp_sonarqube_proxy/cli.py` — Click-CLI mit `serve` (stdio) und `check`
(einmaliger Verbindungstest).
### Zentrale Architekturentscheidungen
1. **Low-Level `Server`, nicht `FastMCP`.** FastMCP introspiziert die
Handler-Signatur, um ein `inputSchema` zu generieren. Bei einem generischen
`**kwargs`-Handler ist das Schema leer und der Client weiss nicht, wie er
das Tool aufrufen soll. Der Low-Level-Server uebernimmt dagegen das
`Tool`-Objekt mit allen Feldern (`inputSchema`, `outputSchema`,
`annotations`, `title`, `_meta`) unveraendert vom Upstream.
2. **Eine persistente Upstream-Session, kein Per-Call-Reconnect.** Die fruehere
Variante hat fuer jeden Tool-Call eine neue HTTP-Session geoeffnet. Das
bricht die Streamable-HTTP-Session-Id, multipliziert die Latenz und macht
`initialize()` zur teuersten Operation im Hot Path. Jetzt: einmal beim Start
verbinden, bei `serve`-Lifetime offenhalten, beim Beenden sauber schliessen.
3. **`validate_input=False` auf dem `@call_tool`-Decorator.** Der Upstream ist
die einzige Wahrheit ueber Schemata. Lokale Validierung wuerde nur
duplizieren und koennte bei Schema-Drift zu falschen Ablehnungen fuehren.
4. **`server.create_initialization_options()` statt manueller Capabilities.**
Die Helper-Methode liefert die korrekten `NotificationOptions()` und
`experimental_capabilities={}` und ueberlebt SDK-Updates besser als ein
handgeschriebener Aufruf.
5. **Stateless Tool-Liste — kein Caching.** Jeder `tools/list`-Request
re-fetcht den Upstream. Weniger Code, immer aktuell, kein Cache-Coherency-
Problem.
### Logging und stdout/stderr
`stdout` ist fuer JSON-RPC reserviert. Jegliche Ausgabe (Logging, Startmeldungen,
Fehlermeldungen) muss auf `stderr`. `_configure_logging` setzt das fuer das
Logging-Modul, `_stderr()` ist der Helper fuer direkte Meldungen.
### SONARQUBE_TOKEN — Sicherheit
Der Upstream verlangt einen User-Token im `Authorization`-Header. Wir lesen
den Token in `proxy.sonarqube_token()` aus der Env-Var, leiten ihn als
`Bearer <token>` an `streamablehttp_client(headers=...)` weiter, und speichern
ihn nirgends. Vor jedem stderr-Output mit potenziell ungeprueftem
Exception-Text laeuft `cli._redact()`, die den live-Token (falls in der Env)
durch `[REDACTED]` ersetzt — defensive Schicht gegen Drittanbieter-Libraries,
die in Fehlermeldungen Header einbetten koennten.
`TokenMissingError` enthaelt **niemals** Token-Material, weil dieser Fall genau
heisst: es gibt keinen Token. Die Meldung enthaelt nur die operator-gerichtete
Anleitung "My Account -> Security".
## Lokale Entwicklung
```bash
# Editable install + Dev-Group
uv sync
# Upstream-Verbindung testen
SONARQUBE_MCP_URL=http://192.168.0.2:8765/mcp uv run mcp-sonarqube-proxy check
# Server lokal starten (stdio — z.B. via MCP Inspector)
SONARQUBE_MCP_URL=http://192.168.0.2:8765/mcp uv run mcp-sonarqube-proxy serve
# Tests
uv run pytest
# Lint / Format
uv run ruff check .
uv run ruff format .
```
## Installation als Tool
```bash
uv tool install git+https://gitea.gecheckt.de/marcus/mcp-sonarqube-proxy.git
```
## Bekannte offene Punkte
- **Reconnect bei Verbindungsabbruch:** Wenn der Upstream waehrend `serve`
weggeht, scheitern nachfolgende Tool-Calls bis zum Neustart des Proxies.
Eine Reconnect-Logik mit Retry on `anyio.ClosedResourceError`/`httpx.ReadError`
waere ein sinnvoller naechster Schritt, ist fuer v0.1 aber bewusst weggelassen.
- **Concurrent Tool-Calls:** Die `ClientSession` korreliert Antworten ueber
Request-IDs und sollte parallele Calls verkraften. Nicht explizit getestet.
- **SDK-Versionssprung:** Auf `main` der python-sdk wurden die Decorators
durch Konstruktor-Kwargs ersetzt. Bei Migration auf eine neue Major-Version
muss `build_proxy_server()` angepasst werden.