feat: Gemini bridge, browser extension hardening, shared memory

Nexus gateway (measured 2026-08-07, not assumed):
- Gemini speaks Google-GenAI (/v1beta/models/{id}:streamGenerateContent, header
  api-key), NOT the Azure-OpenAI path — that returned 404 "no Route matched" and
  was the cause of the reported failures. New agent/gemini_bridge.py translates
  Bedrock Converse <-> Gemini in both directions.
- Only gemini-3.6-flash is subscribed; 2.5-flash/2.5-pro/3.1-flash-lite give 403,
  every other name 404. Catalog corrected.
- Four Gemini rules, each previously an HTTP 400, now covered by tests:
  thought signatures are mandatory, they belong to the TURN (not the individual
  call), functionResponse turns must be homogeneous, arrays need `items`.
- Prompt caching is NOT available: cachePoint is accepted and ignored.

System prompt:
- Was an f-string; a code sample containing braces broke build_system_prompt at
  request time (CLI and web both 500, import stayed green). Now a plain template
  with __TOKEN__ placeholders. Regression guards in tests/test_system_prompt.py.

CLI:
- `agent resume` now prints the stored transcript. The history was always loaded
  into the model context, only the terminal stayed empty.

Memory (new, all three surfaces):
- agent/memory.py stores notes about the user in one local file, written
  atomically; memory.cnull.net remains an optional mirror that can never fail a
  write. Tools memory_save/search/forget, injected into the prompt with a budget.
  HTTP surface /api/memory for the extension.

Browser extension (agent/extension, first commit of the source):
- driveMode 'direct' talks to Nexus without the Python broker: Claude via
  Bedrock converse, GPT via Azure-OpenAI, Gemini via Google-GenAI.
- browser_type no longer guesses the focused element — that wrote whole mails
  into Outlook's subject line. Read-back now reports where the text actually
  landed, so a mis-target is visible instead of silent.
- aria-labelledby is resolved across all ids (it is a list); contenteditable is
  interactive and marked editable. Without this, subject and message body look
  identical to the model.
- Hard block against sending mail, independent of riskMode.
- Runs survive the panel: events are buffered and replayed by sequence number.
- Image input (paste, file, drag&drop), on-page glow/spotlight, memory tools.

Cost: fixed tokens per round 11434 -> 6540 (-43%) by trimming tool schemas,
dropping gateway docs from the browser prompt and sending site knowledge only
where it applies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 17:31:26 +02:00
co-authored by Claude Opus 5
commit f943261c3c
30 changed files with 13575 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
# Nexus MCP-Bruecke
Verbindet externe KI-Clients (**Claude Code**, **Claude Desktop**, **Cursor**) mit der
Chrome/Brave-Extension. Der Client sieht ganz normale MCP-Tools, die Extension fuehrt sie
im echten Browser aus — inklusive echter Maus- und Tastatureingaben ueber CDP.
```
Claude Code / Desktop / Cursor
│ MCP (Streamable HTTP, POST /mcp)
mcp-bridge/server.js ← 127.0.0.1, Token-Pflicht
│ WebSocket (Protokoll aus src/shared/protocol.ts)
Browser-Extension → Chrome/Brave
```
Keine Abhaengigkeiten, kein Build. Node 18+, ESM, WebSocket selbst implementiert.
---
## 1. Starten
```bash
cd extension/mcp-bridge
npm start # oder: node server.js
```
Beim Start erscheint:
```
────────────────────────────────────────────────────────────────────────
Nexus Browser Pilot — MCP-Bruecke 1.0.0
────────────────────────────────────────────────────────────────────────
MCP-Endpunkt http://127.0.0.1:9224/mcp (nur POST)
Health http://127.0.0.1:9224/health
Extension-WS ws://127.0.0.1:9224/?token=<TOKEN>
Token 0cc23cd9-ad03-4993-b96e-de650663259a
(neu erzeugt, gespeichert in …/mcp-bridge/.token)
Tools 31 aus C:\Users\…\MB\tools\schema
```
Der Server lauscht **ausschliesslich auf 127.0.0.1**. Er ist von aussen nicht erreichbar,
auch nicht aus dem lokalen Netz.
---
## 2. Token
Ein Token ist **Pflicht** — es gibt keinen Modus ohne Authentifizierung.
Reihenfolge beim Start:
1. `NEXUS_MCP_TOKEN` aus der Umgebung, falls gesetzt.
2. Sonst der Inhalt von `mcp-bridge/.token`, falls vorhanden (Neustarts machen damit
eingerichtete Clients nicht ungueltig).
3. Sonst wird ein neues per `crypto.randomUUID()` erzeugt, nach `.token` geschrieben
(Rechte 0600) und auf der Konsole ausgegeben.
Jeder HTTP-Request **und** der WebSocket-Upgrade muessen es tragen:
| Weg | Beispiel |
| --- | --- |
| Header | `Authorization: Bearer <TOKEN>` |
| Header | `X-Mcp-Token: <TOKEN>` |
| Query | `?token=<TOKEN>` |
Ohne gueltiges Token: **HTTP 401**, der WebSocket-Upgrade wird abgelehnt.
Eigenes Token vorgeben:
```bash
# bash
NEXUS_MCP_TOKEN=$(uuidgen) node server.js
```
```powershell
# PowerShell
$env:NEXUS_MCP_TOKEN = [guid]::NewGuid().ToString(); node server.js
```
> `.token` gehoert nicht ins Repository. Falls das Verzeichnis versioniert wird, bitte
> `mcp-bridge/.token` in die `.gitignore` aufnehmen.
---
## 3. Extension einrichten
In der Extension unter **Einstellungen** muss die **brokerUrl** auf diese Bruecke zeigen,
also auf **`ws://127.0.0.1:9224`** — und weil auch der WebSocket ein Token verlangt,
mit angehaengtem Token:
```
ws://127.0.0.1:9224/?token=<TOKEN>
```
Der Pfad ist beliebig; `ws://127.0.0.1:9224/ext/ws?token=<TOKEN>` funktioniert genauso.
Entscheidend sind Host, Port und das Token. Die exakte Zeile zum Kopieren steht beim
Serverstart in der Konsole.
Ausserdem sollte `driveMode` auf **`broker`** stehen (Standard). Danach verbindet sich die
Extension selbstaendig und schickt ihr `hello`; im Log der Bruecke erscheint:
```
[07:20:26] Extension-Socket verbunden (Origin: chrome-extension://…). Warte auf hello …
[07:20:26] hello von Extension v1.0.0 (Session-Secret vorhanden). Bereit.
```
Pruefen laesst sich das jederzeit ueber:
```bash
curl "http://127.0.0.1:9224/health?token=<TOKEN>"
# {"status":"ok","extension":"verbunden","tools":31,...}
```
---
## 4. Einbindung in Claude Code
```bash
claude mcp add --transport http browser http://127.0.0.1:9224/mcp \
--header "Authorization: Bearer <TOKEN>"
```
Pruefen:
```bash
claude mcp list
```
Danach stehen in der Sitzung die Tools `browser_navigate`, `browser_screenshot`,
`browser_read_page`, `browser_computer` usw. zur Verfuegung.
Nur fuer das aktuelle Projekt statt global: `--scope project` ergaenzen.
Entfernen: `claude mcp remove browser`.
---
## 5. Einbindung in Claude Desktop
Konfigurationsdatei:
* Windows: `%APPDATA%\Claude\claude_desktop_config.json`
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"browser": {
"type": "http",
"url": "http://127.0.0.1:9224/mcp",
"headers": {
"Authorization": "Bearer <TOKEN>"
}
}
}
}
```
Claude Desktop danach vollstaendig beenden und neu starten (Tray-Icon schliessen, nicht nur
das Fenster).
Sollte die verwendete Version keine HTTP-Server unterstuetzen, hilft die Bruecke ueber
`mcp-remote` weiter:
```json
{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://127.0.0.1:9224/mcp",
"--header", "Authorization: Bearer <TOKEN>"]
}
}
}
```
## Einbindung in Cursor
`.cursor/mcp.json` im Projekt oder `~/.cursor/mcp.json` global:
```json
{
"mcpServers": {
"browser": {
"url": "http://127.0.0.1:9224/mcp",
"headers": { "Authorization": "Bearer <TOKEN>" }
}
}
}
```
---
## 6. Umgebungsvariablen
| Variable | Standard | Bedeutung |
| --- | --- | --- |
| `MCP_PORT` | `9224` | Port auf 127.0.0.1. |
| `NEXUS_MCP_TOKEN` | — | Token vorgeben. Sonst `.token` bzw. neu erzeugt. |
| `MCP_TOOL_TIMEOUT_MS` | `60000` | Wartezeit auf ein `tool_result` der Extension. |
| `MCP_SCHEMA_DIR` | `../../tools/schema` | Verzeichnis mit den `browser_*.json`-Schemata. |
| `MCP_ALLOWED_ORIGIN` | — | Kommagetrennte Liste. Nur dann gibt es ueberhaupt CORS-Header. |
| `MCP_STRICT_SESSION` | `0` | Bei `1` muss jeder Folgeaufruf eine bekannte `Mcp-Session-Id` mitschicken. |
---
## 7. Tools
Die Tool-Liste ist **nicht** im Server hartkodiert. Bei jedem `tools/list` werden alle
`*.json` aus `tools/schema/` gelesen und ueber `name`, `description` und `inputSchema` auf
MCP-Tools abgebildet (`requires_confirmation: true` wird zu `annotations.destructiveHint`).
Ein neues Schema in diesem Verzeichnis steht also ohne Neustart zur Verfuegung; der Pfad
laesst sich mit `MCP_SCHEMA_DIR` umbiegen.
**Screenshots** kommen als echter MCP-Bildinhalt zurueck, nicht als Text:
```json
{"content":[
{"type":"image","data":"<base64 ohne data:-Praefix>","mimeType":"image/jpeg"},
{"type":"text","text":"{ \"image\": \"[Bild #1 als image-Content angehaengt, 118 KB]\", \"width\": 1400, … }"}
]}
```
Das gilt auch fuer Bilder tief in einem `browser_batch`-Ergebnis. Der base64-Block landet
nie im Text und nie im Log.
---
## 8. HTTP-Endpunkte
| Methode | Pfad | Verhalten |
| --- | --- | --- |
| `POST` | `/mcp` | MCP ueber Streamable HTTP: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, `ping`. |
| `GET` | `/mcp` | **405** mit Erklaerung. Bewusst kein SSE-Stream, der offen bleibt und nie antwortet. |
| `DELETE` | `/mcp` | Beendet die Session aus dem Header `Mcp-Session-Id`. |
| `GET` | `/health` | Status, Extension-Verbindung, Tool-Anzahl. |
| `GET` | `/` (Upgrade) | WebSocket fuer die Extension. |
`initialize` erzeugt eine Session-Id, liefert sie im Antwort-Header `Mcp-Session-Id` und
prueft sie bei Folgeaufrufen: eine unbekannte oder abgelaufene Id ergibt **404**, worauf
konforme Clients neu initialisieren.
---
## 9. Sicherheit
Diese Bruecke gibt einem externen Prozess die Kontrolle ueber den angemeldeten Browser des
Nutzers — mit allen Sessions, Cookies und gespeicherten Passwoertern. Entsprechend eng ist
sie zugeschnitten:
* **Nur 127.0.0.1.** Kein Schalter, um auf `0.0.0.0` zu binden.
* **Token ist Pflicht**, fuer HTTP *und* fuer den WebSocket-Upgrade. Verglichen wird in
konstanter Zeit ueber SHA-256-Digests.
* **Kein CORS-Wildcard.** `Access-Control-Allow-Origin` wird nur gesetzt, wenn
`MCP_ALLOWED_ORIGIN` ausdruecklich einen passenden Origin nennt. Ohne die Variable gibt
es gar keinen CORS-Header, und Preflights werden mit 403 beantwortet.
* **WS-Upgrade nur mit passendem Origin:** entweder gar keiner (lokale Prozesse) oder
`chrome-extension://…`. Eine besuchte Webseite sendet immer ihren eigenen Origin und
kann ihn nicht faelschen — damit ist der Weg "boesartige Seite oeffnet
`ws://127.0.0.1:9224`" versperrt.
* **Genau eine Extension-Verbindung.** Meldet sich eine zweite, wird die bestehende
*sauber* mit Close-Code 1012 geschlossen und protokolliert, offene Aufrufe werden mit
klarer Meldung abgebrochen — nichts wird stillschweigend ersetzt.
* **Kein `hello`, keine Tools.** Eine Verbindung, die sich nicht innerhalb von 15 Sekunden
mit `hello` meldet, wird getrennt und bekommt nie einen `tool_call` zu sehen.
* **Keine Geheimnisse im Log.** Weder das Session-Secret der Extension noch base64-Bilddaten
werden ausgegeben.
> Der Vorlage `agent_workspace/browser-pilot/server/index.js` fehlte genau das: `/execute`
> war ungeschuetzt und `Access-Control-Allow-Origin: *` erlaubte jeder besuchten Webseite,
> den Browser per `fetch()` fernzusteuern. Diese Kombination ist hier bewusst nicht
> nachgebaut — es gibt kein `/execute` und kein Wildcard.
Das Token ist ein Passwort. Es gehoert nicht in ein Repository, nicht in ein Ticket und
nicht in eine Bildschirmaufnahme.
---
## 10. Fehlersuche
| Symptom | Ursache und Abhilfe |
| --- | --- |
| `401 unauthorized` | Token fehlt oder ist falsch. Aktuelles Token: Konsole beim Start oder `mcp-bridge/.token`. |
| `Keine Extension verbunden` | Browser offen? Extension aktiv? brokerUrl inkl. `?token=` gesetzt? `driveMode` auf `broker`? |
| `Port 9224 ist bereits belegt` | Die Bruecke laeuft schon, oder ein alter Prozess haengt. Anderen Port ueber `MCP_PORT`. |
| `Zeitueberschreitung nach 60000 ms` | Die Extension antwortet nicht — offene Risiko-Rueckfrage im Side Panel, blockierender Browser-Dialog oder kein aktiver Tab. Notfalls `MCP_TOOL_TIMEOUT_MS` erhoehen. |
| `Unbekannte oder abgelaufene Mcp-Session-Id` | Der Client haelt eine Session von einem frueheren Serverlauf. Client neu verbinden lassen. |
| `tools/list` liefert 0 Tools | `tools/schema/` nicht gefunden. `MCP_SCHEMA_DIR` setzen; `/health` zeigt den benutzten Pfad. |
| `GET /mcp` gibt 405 | So gewollt. MCP laeuft ueber `POST /mcp`. |
Schnelltest ohne Client:
```bash
TOKEN=$(cat .token)
curl -s -X POST http://127.0.0.1:9224/mcp \
-H "content-type: application/json" \
-H "authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400
```