second-brain-worker
by 50R1Paps
README.md
# Second Brain Worker
Un knowledge base personale **serverless** su Cloudflare Workers che indicizza la tua [wiki Obsidian](https://github.com/50R1Paps/Mysecondbrain) e file esterni, con ricerca ibrida (semantica + keyword) accessibile via **MCP (Model Context Protocol)** direttamente dal tuo IDE.
## Cos'è
Second Brain Worker è un Cloudflare Worker che:
- **Indicizza** file markdown dalla wiki Obsidian e file esterni (PDF, articoli, testo)
- **Chunka** il contenuto rispettando la struttura delle sezioni (`##`/`###`), preservando frontmatter e wikilink
- **Genera embedding** semantici via Workers AI (`@cf/baai/bge-base-en-v1.5`, 768 dim)
- **Memorizza** tutto in R2 (raw), D1 (metadata + FTS5 keyword search), e Vectorize (embedding)
- **Espone 5 tool MCP** — `retrieve`, `ingest`, `reindex`, `read`, `grep` — accessibili dal tuo IDE via OAuth GitHub
- **Sincronizza** automaticamente la wiki via GitHub webhook: quando fichi push su `main`, i file `.md` cambiati vengono re-indicizzati
Tutto entro il **free tier** di Cloudflare.
---
## Architettura
```
┌─────────────────────────────────────────────┐
│ Cloudflare Worker │
│ │
GitHub Webhook ──▶ /webhook/github ──▶ fetch .md ──▶ R2 │
│ │ │
REST API ────────▶ /api/ingest ──▶ chunker │ │
│ /api/retrieve ──▶ embedder │ │
│ /api/reindex ──▶ D1 + Vectorize │
│ /api/health │ │ │
│ │ │ │
MCP Client ─────▶ /mcp (OAuth) │ │ │
│ /authorize │ │ │
│ /callback │ │ │
└─────────────────────────────────────────────┘
│ │ │
┌──────▼──┐ ┌───▼────┐ ┌────▼──────┐
│ R2 │ │ D1 │ │ Vectorize │
│ (raw) │ │(meta + │ │ (embedding│
│ │ │ FTS5) │ │ vectors) │
└─────────┘ └────────┘ └───────────┘
```
### Componenti
- **R2** (`RAW_BUCKET`) — storage del contenuto raw dei file
- **D1** (`DB`) — database SQLite con tabelle `files`, `chunks`, e tabella virtuale FTS5 `chunks_fts` per keyword search
- **Vectorize** (`VECTORIZE`) — indice di embedding semantici (768 dimensioni)
- **Workers AI** (`AI`) — modello `bge-base-en-v1.5` per generazione embedding
- **KV** (`OAUTH_KV`) — storage di stato/CSRF token per il flow OAuth
- **Durable Object** (`SECOND_BRAIN_MCP`) — McpAgent che mantiene la sessione MCP
- **OAuth Provider** — integrazione `@cloudflare/workers-oauth-provider` con GitHub OAuth
---
## MCP Tools
Il server MCP espone 5 tool accessibili dal tuo IDE:
### `retrieve`
Ricerca ibrida nel knowledge base: query semantica via Vectorize + query keyword via D1 FTS5. I risultati vengono fusi (merge) con pesi 50/50, ordinati per score combinato, e arricchiti con metadata dal D1.
**Parametri:**
- `query` (string, required) — query in linguaggio naturale o keyword
- `limit` (number, default 10, max 50) — numero massimo di risultati
- `file_type` (`"wiki_page"` | `"ingested"`, optional) — filtra per tipo di file
- `section_prefix` (string, optional) — filtra per prefisso sezione (es. `## Architecture`)
- `file_key_prefix` (string, optional) — filtra per prefisso file key (es. `wiki/concepts/`)
### `ingest`
Carica un file nel knowledge base: il contenuto viene chunkato, embeddato, e salvato in R2 + D1 + Vectorize. Se il file esiste già, i vecchi chunk e vector vengono sostituiti.
**Parametri:**
- `file_key` (string, required) — identificatore univoco. Per wiki page: path relativo (es. `wiki/concepts/Tool Attention.md`). Per file esterni: `filename:uuid`
- `content` (string, required) — contenuto testuale completo
- `file_type` (`"wiki_page"` | `"ingested"`, required) — tipo di file
- `title` (string, optional) — titolo del file
- `source` (string, optional) — URL o path sorgente
### `reindex`
Re-indicizza un file specifico o tutti i file. Legge il raw da R2, re-esegue chunking + embedding, aggiorna Vectorize e D1.
**Parametri:**
- `file_key` (string, optional) — file da re-indicizzare. Se omesso, re-indicizza tutti i file
### `read`
Legge il testo raw di un file indicizzato da R2 con offset/limite opzionali. Utile per leggere il contesto completo attorno a un chunk trovato via retrieve.
**Parametri:**
- `file_key` (string, required) — file key del file da leggere
- `offset` (number, optional, default 0) — offset in caratteri da cui iniziare
- `max_chars` (number, optional, default 2000, max 10000) — numero massimo di caratteri da restituire
### `grep`
Cerca un pattern regex nel testo raw di un file indicizzato. Restituisce i match con contesto opzionale. Utile per estrarre dati strutturati (date, importi, ID) dai documenti.
**Parametri:**
- `file_key` (string, required) — file key del file in cui cercare
- `pattern` (string, required) — pattern regex JavaScript
- `max_matches` (number, optional, default 10, max 50) — numero massimo di match
- `context` (number, optional, default 40, max 200) — caratteri di contesto attorno a ogni match
---
## REST API
Oltre ai tool MCP, il Worker espone endpoint REST (non autenticati, utili per script e setup):
| Metodo | Endpoint | Descrizione |
| ------ | ----------------- | ----------------------------------------------------------------- |
| GET | `/api/health` | Stato del Worker: file indicizzati, chunk totali |
| POST | `/api/ingest` | Ingest di un file (stesso formato del tool MCP) |
| POST | `/api/retrieve` | Retrieve ibrido (stesso formato del tool MCP) |
| POST | `/api/reindex` | Reindex di un file o di tutti |
| POST | `/api/read` | Leggi raw text da R2 con offset/limit |
| POST | `/api/grep` | Ricerca regex sul contenuto indicizzato |
| GET | `/api/metrics` | Metriche aggregate di retrieve (latency, score, zero-result rate) |
| POST | `/webhook/github` | Webhook GitHub per sync automatico |
---
## Prerequisiti
- **Node.js** 22+
- **Account Cloudflare** (free tier sufficiente)
- **GitHub OAuth App** — per l'autenticazione MCP
- **Wiki Obsidian** in un repo GitHub (se vuoi usare il sync automatico)
---
## Setup completo
### 1. Clona e installa
```bash
git clone https://github.com/50R1Paps/second-brain-worker.git
cd second-brain-worker
npm install
```
### 2. Crea le risorse Cloudflare
Esegui questi comandi con `wrangler` per creare le risorse necessarie:
```bash
# Crea R2 bucket
npx wrangler r2 bucket create second-brain-raw
# Crea D1 database
npx wrangler d1 create second-brain
# Annota il database_id dal output
# Crea KV namespace per OAuth
npx wrangler kv namespace create OAUTH_KV
# Annota l'id dal output
# Crea Vectorize index (768 dim per bge-base-en-v1.5)
npx wrangler vectorize create second-brain-embeddings --dimensions 768 --metric cosine
```
### 3. Configura `wrangler.toml`
Copia il file di esempio e compila i valori reali:
```bash
cp wrangler.toml.example wrangler.toml
```
Modifica `wrangler.toml` sostituendo `<your-d1-database-id>` e `<your-kv-namespace-id>` con i valori ottenuti al passo 2.
### 4. Applica la migration D1
```bash
# Locale (per dev)
npm run db:migrate
# Remoto (per produzione)
npm run db:migrate:remote
```
### 5. Crea una GitHub OAuth App
1. Vai su [GitHub Settings > Developer settings > OAuth Apps > New OAuth App](https://github.com/settings/developers)
2. Compila:
- **Application name:** Second Brain MCP
- **Homepage URL:** `https://second-brain.<tuo-subdomain>.workers.dev`
- **Authorization callback URL:** `https://second-brain.<tuo-subdomain>.workers.dev/callback`
3. Annota il **Client ID** e genera un **Client Secret**
### 6. Imposta i segreti
```bash
# OAuth
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY
# GitHub webhook sync (opzionale, solo se usi il sync automatico)
npx wrangler secret put WEBHOOK_SECRET
npx wrangler secret put GITHUB_TOKEN
npx wrangler secret put GITHUB_TOKEN_EXPIRY
```
Per `COOKIE_ENCRYPTION_KEY` puoi generare una stringa casuale con:
```bash
openssl rand -hex 32
```
Per `GITHUB_TOKEN`, crea un [Personal Access Token](https://github.com/settings/tokens) con scope `repo` (per leggere i file `.md` via API).
Per `WEBHOOK_SECRET`, genera un'altra stringa casuale e usala anche come secret del webhook GitHub.
Per `GITHUB_TOKEN_EXPIRY`, inserisci la data di scadenza del PAT in formato ISO (es. `2026-09-25T00:00:00Z`). Il Cron Trigger giornaliero controlla questa data e apre automaticamente una issue su [second-brain-worker](https://github.com/50R1Paps/second-brain-worker/issues) 2 giorni prima della scadenza come promemoria.
### 7. Deploy
```bash
npm run deploy
```
Annota l'URL del Worker (es. `https://second-brain.<tuo-subdomain>.workers.dev`).
### 8. Inizializza il knowledge base (setup script)
Se hai la wiki Obsidian in locale, puoi ingerire tutti i file `.md` con lo script di setup:
```bash
# Locale (durante dev)
npm run setup -- --wiki-dir /path/to/wiki --url http://localhost:8787
# Remoto (dopo deploy)
npm run setup -- --wiki-dir /path/to/wiki --url https://second-brain.<tuo-subdomain>.workers.dev
# Dry run (lista file senza ingerire)
npm run setup:dry -- --wiki-dir /path/to/wiki
```
### 9. Configura il webhook GitHub (opzionale)
Per sincronizzare automaticamente la wiki quando fichi push su `main`:
1. Vai su [GitHub > Your Repo > Settings > Webhooks > Add webhook](https://github.com/50R1Paps/Mysecondbrain/settings/hooks/new)
2. Compila:
- **Payload URL:** `https://second-brain.<tuo-subdomain>.workers.dev/webhook/github`
- **Content type:** `application/json`
- **Secret:** lo stesso valore di `WEBHOOK_SECRET`
- **Trigger:** "Just the push event"
3. Salva
Da ora, ogni push su `main` che modifica file `.md` in `wiki/` triggera la re-indicizzazione automatica.
---
## Gestione dei secret
I secret sono gestiti via `wrangler secret put` e stored nella dashboard Cloudflare (mai nel codice). Ecco quando aggiornarli:
| Secret | Scade? | Quando aggiornare |
| ----------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `GITHUB_TOKEN` | **Sì** — scade in base alla configurazione del PAT | Quando il Personal Access Token scade: `npx wrangler secret put GITHUB_TOKEN` e incolla il nuovo token |
| `GITHUB_TOKEN_EXPIRY` | **Sì** — da aggiornare insieme al token | Quando aggiorni `GITHUB_TOKEN`: `npx wrangler secret put GITHUB_TOKEN_EXPIRY` con la nuova data di scadenza |
| `GITHUB_CLIENT_ID` | No | Solo se revochi/ricrei l'OAuth App su GitHub |
| `GITHUB_CLIENT_SECRET` | No | Solo se revochi/ricrei l'OAuth App su GitHub |
| `COOKIE_ENCRYPTION_KEY` | No | Mai (a meno che tu non voglia invalidare tutte le sessioni attive) |
| `WEBHOOK_SECRET` | No | Mai (deve coincidere con il secret configurato nelle impostazioni webhook su GitHub) |
### Rotazione del `GITHUB_TOKEN`
Il `GITHUB_TOKEN` (Personal Access Token con scope `repo`) è l'unico secret con scadenza. Un Cron Trigger giornaliero controlla `GITHUB_TOKEN_EXPIRY` e apre automaticamente una issue su [second-brain-worker](https://github.com/50R1Paps/second-brain-worker/issues) 2 giorni prima della scadenza come promemoria.
Quando ricevi la notifica (o quando il token è già scaduto):
1. Crea un nuovo token su [GitHub Settings > Tokens](https://github.com/settings/tokens) con scope `repo`
2. Aggiorna i secret su Cloudflare:
```bash
npx wrangler secret put GITHUB_TOKEN
npx wrangler secret put GITHUB_TOKEN_EXPIRY
```
3. Incolla il nuovo token e la nuova data di scadenza (formato ISO, es. `2026-09-25T00:00:00Z`)
4. Chiudi la issue di promemoria su GitHub
Nessun altro secret o configurazione su Cloudflare deve essere aggiornato.
---
## Configurare il client MCP nel IDE
### Windsurf / Cursor
Crea o modifica il file `mcp_config.json` nel tuo IDE (in Windsurf: Settings > MCP Servers):
```json
{
"mcpServers": {
"second-brain": {
"command": "npx",
"args": [
"workers-mcp",
"proxy",
"https://second-brain.<tuo-subdomain>.workers.dev/mcp"
]
}
}
}
```
Sostituisci `<tuo-subdomain>` con il tuo subdomain reale.
Al primo utilizzo, il IDE aprira il browser per l'autenticazione GitHub. Dopo il login, i 5 tool (`retrieve`, `ingest`, `reindex`, `read`, `grep`) saranno disponibili nell'AI assistant.
### Claude Desktop
Aggiungi al file `claude_desktop_config.json`:
```json
{
"mcpServers": {
"second-brain": {
"command": "npx",
"args": [
"workers-mcp",
"proxy",
"https://second-brain.<tuo-subdomain>.workers.dev/mcp"
]
}
}
}
```
---
## Sviluppo
### Comandi disponibili
| Comando | Descrizione |
| --------------------------- | -------------------------------------------- |
| `npm run dev` | Avvia il Worker in locale con `wrangler dev` |
| `npm run deploy` | Deploy su Cloudflare |
| `npm test` | Esegue i test (vitest) |
| `npm run test:watch` | Test in watch mode |
| `npm run typecheck` | Type checking con `tsc --noEmit` |
| `npm run db:migrate` | Applica migration D1 in locale |
| `npm run db:migrate:remote` | Applica migration D1 in remoto |
| `npm run setup` | Script di ingest della wiki |
| `npm run setup:dry` | Dry run dello script di setup |
### Struttura del progetto
```
src/
├── worker.ts # Entry point: OAuthProvider + routing
├── mcp.ts # SecondBrainMCP (McpAgent) con i 5 tool
├── types.ts # Tipi condivisi: Env, request/response interfaces
├── http.ts # Utility HTTP: jsonResponse, handleCORS
├── health.ts # Health check endpoint
├── ingest.ts # Ingestion: chunking, embedding, R2+D1+Vectorize, GitHub push
├── retrieve.ts # Retrieve ibrido: semantic + keyword search, merge, metrics
├── metrics.ts # Metriche aggregate di retrieve (latency, score, zero-result)
├── reindex.ts # Reindex singolo file o tutti i file
├── read.ts # Read raw text da R2 con offset/limit
├── grep.ts # Grep regex sul contenuto indicizzato
├── chunker.ts # Markdown chunker (split su ##/###, overlap, wikilink-safe)
├── github-handler.ts # Hono app: REST API + OAuth flow (/authorize, /callback)
├── oauth-utils.ts # Utility OAuth: state, CSRF, cookie, approval dialog
├── webhook.ts # GitHub webhook handler per sync automatico
├── cron.ts # Cron Trigger: reminder scadenza GITHUB_TOKEN via GitHub Issue
└── setup.ts # Logica del setup script (ingest bulk)
migrations/
├── 0001_initial_schema.sql # Schema D1: files, chunks, chunks_fts + trigger
└── 0002_retrieve_metrics.sql # Tabella retrieve_metrics per observability
scripts/
└── setup.ts # CLI entry point per il setup script
test/ # Test suite (vitest + @cloudflare/vitest-pool-workers)
```
### Test
```bash
npm test # tutti i test
npm run typecheck # type checking
```
I test usano `@cloudflare/vitest-pool-workers` per simulare l'ambiente Workers con D1, R2, e AI bindings.
---
## Operazioni di manutenzione
### Deploy del Worker
Dopo aver modificato il codice:
```bash
npm run typecheck # verifica tipi
npm test # verifica test
npx wrangler deploy # deploy su Cloudflare
```
Non serve reindicizzare dopo un deploy che non cambia la logica di ingest/retrieve.
### Applicare una nuova migration D1
Quando aggiungi una migration (es. `0002_retrieve_metrics.sql`):
```bash
# Locale (per dev)
npm run db:migrate
# Remoto (per produzione)
npx wrangler d1 migrations apply second-brain --remote
```
La migration va applicata **prima** del deploy se il codice dipende dalla nuova tabella.
### Reindicizzare i documenti (popolare Vectorize)
Se i vettori sono mancanti o vuoi rigenerare gli embeddings:
```bash
# Reindex di un singolo file
curl -X POST https://second-brain.<tuo-subdomain>.workers.dev/api/reindex \
-H "Content-Type: application/json" \
-d '{"file_key": "wiki/concepts/Esempio.md"}'
# Reindex di tutti i file (attenzione: può hit rate limit su Workers AI)
curl -X POST https://second-brain.<tuo-subdomain>.workers.dev/api/reindex \
-H "Content-Type: application/json" \
-d '{}'
```
**Nota:** il reindex di tutti i file (113+) può fallire per rate limiting di Workers AI.
Se `semantic_hits` è 0 dopo un reindex completo, reindicizza i file singolarmente
con una pausa di ~1 secondo tra ogni chiamata. Verifica lo stato con:
```bash
npx wrangler d1 execute second-brain --remote \
--command "SELECT COUNT(*) as total, COUNT(vector_id) as with_vectors FROM chunks"
```
### Verificare le metriche di retrieve
Dopo aver fatto qualche retrieve, controlla le metriche aggregate:
```bash
# Ultime 24 ore (default)
curl https://second-brain.<tuo-subdomain>.workers.dev/api/metrics
# Ultima ora
curl https://second-brain.<tuo-subdomain>.workers.dev/api/metrics?period=1h
# Ultimi 7 giorni
curl https://second-brain.<tuo-subdomain>.workers.dev/api/metrics?period=7d
# Ultimi 30 giorni
curl https://second-brain.<tuo-subdomain>.workers.dev/api/metrics?period=30d
```
Periodi validi: `1h`, `24h`, `7d`, `30d`.
Le metriche includono: total queries, zero-result rate, avg/p50/p95 latency,
score distribution, e search type breakdown (semantic/keyword/hybrid).
---
## Documenti
- [PRD](.context/prd.md)
- [Architecture](.context/architecture.md)
- [UI Specification](.context/ui_specification.md)
- [Tasks](.context/tasks.md)
- [ADR-0001](docs/adr/0001-second-brain-serverless-architecture.md)
---
## Limiti e note
- Il chunker e ottimizzato per **markdown** con frontmatter YAML e wikilink `[[Nome]]` (formato Obsidian)
- L'embedding model `bge-base-en-v1.5` produce vettori a 768 dimensioni — assicurati che il Vectorize index sia creato con `--dimensions 768`
- Il retrieve ibrido usa pesi 50% semantico + 50% keyword, con top-K = 20 per ciascuna modalita
- Il content dei risultati viene troncato a 2000 caratteri per chunk
- L'OAuth flow supporta solo GitHub come identity provider
- Il webhook sync processa solo file `.md` nella cartella `wiki/` su push a `main`
- I commit automatici del Worker su GitHub includono `[skip ci]` per non triggerare la pipeline di lint
- Le metriche di retrieve sono best-effort: se la persistenza su D1 fallisce, il retrieve non viene interrotto
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessResponsive