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:
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user