# Projekt Nostr → Graf → Serwery MCP

Dokument roboczy z pierwszej sesji projektowej. Zawiera **finalne decyzje architektoniczne** i model danych. Kolejna rozmowa może zacząć od implementacji.

---

## 1. Cel systemu

Budujemy **dwa serwery MCP** nad wspólną bazą grafową z danymi Nostr:

1. **Client MCP server (zapytania)** — dla zewnętrznych asystentów AI. Klient konstruuje Cypher/Python na podstawie polecenia użytkownika i odczytuje wynik. Read-only nad grafem danych.
2. **Admin MCP server (zarządzanie)** — dla operatora systemu. Pozwala zarządzać ingestem: dodawać/usuwać relays, definiować subskrypcje (kindy, autorzy, zakresy czasu), uruchamiać backfille, oglądać status. Write do konfiguracji ingestu.

Obydwa serwery to osobne procesy z osobnymi entry pointami, współdzielące sterownik Neo4j i ten sam model danych.

Nie budujemy: STT, GUI, konkretnego klienta asystenta. To jest po stronie klientów MCP — my dostarczamy dane, narzędzia zapytań i narzędzia zarządzania.

Jednozdaniowe podsumowanie:
> Dwa serwery MCP nad wspólnym grafem Nostr w Neo4j: **client** (read: Cypher/Python na grafie) i **admin** (write: konfiguracja ingestu — relays, subskrypcje, kontrola procesu).

---

## 2. Stack technologiczny (ZAMROŻONY)

- **Baza grafowa:** Neo4j Community Edition (Docker) — **bez APOC**
  - Powody wyboru Neo4j: Cypher jest standardem (LLM pisze go najlepiej → mniej retry), Neo4j Browser do debugu, ogromny ekosystem/przykłady w danych treningowych LLM.
  - Kuzu był rozważany i odrzucony: embedded byłby prostszy, ale ryzyko częstszych błędów w Cypher generowanym przez LLM (mniej przykładów treningowych). Retry w kliencie = gorszy UX.
  - **APOC odrzucony świadomie:** flagowy `apoc.convert.toTree()` nie pasuje do naszego modelu z powodu warstwy Tag/TagElement (zostawia nody pośrednie w drzewie). Inne funkcje APOC to głównie quality-of-life wins, które da się zastąpić Pythonem. Rezygnacja z APOC = trywialna migracja do Kuzu w przyszłości + czystsza dokumentacja capabilities dla LLM.
- **Język:** Python end-to-end
- **Ingest z Nostr:** `pynostr` (lub `nostr-sdk` Python bindings) → sterownik Neo4j (`neo4j` oficjalny). Ingest i MCP server to dwa osobne entry pointy współdzielące kod mapowania (szczegóły w sekcji 6).
- **MCP servers:** Python, oficjalne SDK MCP — **dwa osobne procesy** (client i admin), dzielą tylko sterownik Neo4j i kod dostępu do wspólnego modelu danych

Licencja Neo4j Community: GPL v3 (OK dla projektu open source; do weryfikacji jeśli kiedyś komercja).

---

## 3. Model grafu (FINALNY, w języku angielskim)

### Typy węzłów

| Węzeł | Property | Znaczenie |
|---|---|---|
| `Event` | `id`, `kind`, `created_at`, `content`, `sig`, `raw` | Event Nostr |
| `Pubkey` | `hex` | Autor lub pubkey wskazywany przez tag |
| `Relay` | `url` | Relay Nostr |
| `Addressable` | `value`, `kind`, `pubkey`, `d` | Byt adresowalny NIP-33 (`kind:pubkey:d`) |
| `Tag` | `tag_id`, `name`, `position_in_event` | Pojedynczy tag eventu |
| `TagElement` | `element_id`, `position`, `text` | Pojedynczy element listy w tagu |

### Typy relacji

| Relacja | Kierunek | Znaczenie |
|---|---|---|
| `AUTHORED_BY` | Event → Pubkey | Autor eventu |
| `SEEN_ON` | Event → Relay | Event widziany w relay (może być wiele) |
| `HAS_TAG` | Event → Tag | Event ma tag |
| `HAS_ELEMENT` | Tag → TagElement | Tag ma element listy |
| `REFERS_TO` | Tag → (Event \| Pubkey \| Addressable \| Relay) | Tag wskazuje konkretny obiekt |

### Reguły interpretacji

- Kolejność tagów w evencie wynika z `Tag.position_in_event`; kolejność elementów taga — z `TagElement.position`.
- `Tag.name` to pomocniczy skrót pierwszego elementu taga.
- Przy `REFERS_TO` typ wskazywanego obiektu wynika z targetu relacji (szczegóły mapowania — sekcja 4).
- Znaczenie pozycji elementów taga wynika z NIP-ów i kontekstu `kind` — ta wiedza jest po stronie klienta (LLM).

---

## 4. Mapowanie eventu Nostr do grafu

Dla każdego eventu, ingest wykonuje:

1. Tworzy/znajduje węzeł `Event` (klucz: `id`).
2. Tworzy/znajduje węzeł `Pubkey` dla autora.
3. Tworzy relację `AUTHORED_BY` (Event → Pubkey).
4. Tworzy/znajduje węzeł `Relay`, dodaje relację `SEEN_ON` (Event → Relay). Ta relacja może istnieć wielokrotnie (ten sam event widziany na różnych relays).
5. Dla każdego taga:
   - Tworzy osobny węzeł `Tag` (`position_in_event` zachowuje kolejność tagów na liście).
   - Dodaje `HAS_TAG` (Event → Tag).
   - Dla każdego elementu taga tworzy `TagElement` (`position` zachowuje kolejność) i dodaje `HAS_ELEMENT` (Tag → TagElement).
   - Jeśli `tag[0]` to konwencja referencyjna (poniżej), tworzy/znajduje obiekt docelowy i dodaje `REFERS_TO` (Tag → obiekt).

### Konwencje `REFERS_TO` (znane nazwy tagów)

| `tag[0]` | Target |
|---|---|
| `e` | `Event` po `id = tag[1]` |
| `p` | `Pubkey` po `hex = tag[1]` |
| `a` | `Addressable` — parsowanie `kind:pubkey:d` z `tag[1]` |
| `r` | `Relay` po `url = tag[1]` |
| inne (`t`, `d`, `subject`, custom) | brak `REFERS_TO` — tylko TagElementy |

### Filozofia: ingest jest kind-agnostic

- **Ingest NIE rozumie znaczenia `kind`.** Przechowuje `Event.kind` jako zwykłą liczbę.
- Jeden generyczny mapper pokrywa 100% eventów (znanych i nieznanych kindów).
- Nowy NIP/kind → **zero zmian w ingeście i schemacie**.
- `Event.content` nie jest parsowane przy ingeście (nawet dla kind 0 — profile JSON zostaje surowym stringiem).
- Ingest ma minimalną wiedzę tylko o **konwencjach nazw tagów** (`e`/`p`/`a`/`r`), nie o kindach.

**Znaczenie kindów i semantyka NIP-ów** to wiedza po stronie klienta MCP (LLM ma ją z treningu / z innych źródeł). Serwer MCP tej wiedzy NIE wystawia.

---

## 5. Architektura MCP (CO WYSTAWIAMY)

Dwa osobne serwery MCP — każdy z własnym zbiorem tools/resources i własnym typem klienta.

### 5.1 Client MCP server (warstwa zapytań)

**Założenie kluczowe:** klient (dowolny zewnętrzny LLM) przychodzi z własną wiedzą o Nostr, NIP-ach, znaczeniu kindów, konwencjach tagów. My dostarczamy tylko to, co **specyficzne dla naszego systemu**: schemat grafu i narzędzia wykonawcze.

#### Tools (narzędzia wykonawcze)

```
run_cypher(query: str, params: dict = {}) -> list[dict]
    - wykonuje Cypher przez sterownik Neo4j
    - zwraca records jako JSON-serializable listę dictów

run_python(code: str) -> Any
    - exec() w sandboxie z wstrzykniętą aktywną sesją Neo4j (zmienna `session`)
    - kod MUSI przypisać wynik do zmiennej `wynik`
    - zwraca `wynik` jako JSON
```

Obydwa tools operują przez read-only credentials Neo4j — client MCP nie może pisać do grafu ani do konfiguracji (patrz 5.2).

#### Resources

```
nostr://schema/graph
    - opis modelu grafu specyficznego dla NASZEGO systemu:
      * węzły i ich properties
      * relacje i ich kierunki
      * reguły mapowania eventu → graf
      * konwencje REFERS_TO dla tagów e/p/a/r
      * dostępne capabilities (patrz niżej)
    - w formacie zjadliwym dla LLM (Markdown lub structured JSON)
    - to jest JEDYNA rzecz, której LLM nie wie z innych źródeł
```

**Capabilities deklarowane w resource:**
- Czysty Cypher 5.x (Neo4j)
- `run_python` z wstrzykniętą sesją Neo4j + standard library
- **APOC nie jest dostępny** — LLM jawnie informowany, żeby nie próbował (inaczej retry po błędzie = stracone tokeny i opóźnienie)
- Dla drzew o dowolnej głębokości — wzorzec `run_python` z budowaniem drzewa z płaskiej listy par (przykład w resource)

**Świadomie NIE wystawiamy:** semantyki kindów, opisów NIP-ów, konwencji tagów — LLM ma to z treningu.

#### Prompts (opcjonalnie, do rozważenia później)

Można dodać gotowy szablon `query_graph`, który osadza resource `nostr://schema/graph` i instrukcję "napisz Cypher lub Python dla: {user_input}". Ale nie jest to konieczne — klient może sam złożyć taki prompt.

#### Detale implementacyjne tools

- **Sandbox `run_python`:** `exec(code, globals_dict)`, gdzie `globals_dict` zawiera `session` (aktywną sesję Neo4j). Po wykonaniu czytamy `globals_dict["wynik"]`. Jeśli nie można zserializować (np. surowy `neo4j.Node`) — mapujemy na dict; w razie błędu zwracamy czytelny komunikat dla klienta.
- **Timeout:** ~30s per query.
- **Błędy:** zwracamy do klienta pełną treść wyjątku — LLM po stronie klienta potrafi poprawić zapytanie w kolejnej iteracji.
- **Bezpieczeństwo:** nie jest priorytetem (projekt prototypowy, lokalny). Ale cap na timeout i rozmiar wyniku — tak.

#### Dlaczego Python + osadzony Cypher, nie tylko Cypher lub tylko Python

Klient generuje **Python, który zawiera Cypher jako string** i wykonuje przez sterownik. Daje to:
- Wydajność i zwięzłość Cypher tam, gdzie jest mocny (agregacje, patterny, traversale).
- Elastyczność Pythona tam, gdzie Cypher słabnie (parsing `content`, regex, post-processing drzew).
- Klient LLM decyduje per zapytanie, jaką proporcję użyć.

Alternatywa: `run_cypher` wystarczyłby w 85% przypadków. `run_python` to furtka na pozostałe 15% (parsowanie JSON-content, złożona logika, zewnętrzne biblioteki).

### 5.2 Admin MCP server (warstwa zarządzania)

Serwer operatorski — używany przez nas (np. z Claude Desktop) do konfigurowania, co obserwujemy, bez ręcznej edycji configów.

**Założenie kluczowe:** klient admin MCP nie musi znać Nostr ani modelu grafu — wywołuje konkretne tools zarządzające. Tools mają wysoki poziom abstrakcji (np. "dodaj subskrypcję na kindy X od autora Y"), nie ekspozycję Cypher.

#### Tools (zakres funkcjonalny — dokładne API do doprecyzowania)

```
# Relays
add_relay(url: str) -> dict
remove_relay(url: str) -> dict
list_relays() -> list[dict]

# Subskrypcje (filtry w formacie NIP-01: kinds, authors, #e, #p, since, until, limit)
add_subscription(filter: dict) -> str           # zwraca subscription_id
remove_subscription(id: str) -> bool
list_subscriptions() -> list[dict]

# Akcje jednorazowe
backfill(filter: dict) -> dict                   # historyczny pull, zwraca stats

# Kontrola procesu
start_ingest() / stop_ingest() -> bool

# Obserwowalność
status() -> dict                                 # uptime, connection status relays, last seen event
stats() -> dict                                  # counts: eventy per kind, pubkeyi, tagi, relacje
```

#### Resources

```
nostr://admin/config    — bieżąca konfiguracja (relays, subskrypcje) jako JSON
nostr://admin/status    — live status ingestu (uptime, connection, last seen)
```

#### Zapis konfiguracji

Gdzie żyje konfiguracja ingestu? Propozycja domyślna: **w tej samej bazie Neo4j**, pod osobnym namespace'em labeli (np. `:Config:Relay`, `:Config:Subscription`), żeby nie wprowadzać drugiego storage. Ingest czyta konfigurację z Neo4j przy starcie i reaguje na zmiany (poll co N sekund lub Neo4j change feed). Alternatywy (JSON na dysku, SQLite) — do rozważenia, jeśli Neo4j okaże się niewygodny. Decyzja: patrz sekcja 8.

Węzły `:Config:*` są rozłączne od grafu danych (`Event`/`Pubkey`/`Relay`/`Addressable`/`Tag`/`TagElement`) — client MCP ich nie widzi.

#### Separacja uprawnień

- **Client MCP** → Neo4j user z rolą READ, ograniczony do labeli grafu danych.
- **Admin MCP** → Neo4j user z prawem write do labeli `:Config:*` oraz read do reszty (do `stats()`).
- **Ingest** → Neo4j user z prawem write do grafu danych + read do `:Config:*`.

Trzy osobne credentials, ta sama baza.

---

## 6. Komponenty projektu

Trzy osobne komponenty nad wspólną bazą Neo4j:

**A. Ingest Nostr → Neo4j** — daemon. Łączy się z relays zgodnie z konfiguracją czytaną z Neo4j (`:Config:*`, patrz 5.2), pobiera eventy, mapuje do grafu według reguł z sekcji 4. Reaguje na zmiany konfiguracji w runtime.

**B. Client MCP server** — read-only warstwa zapytań nad grafem danych. Tools i resources z sekcji 5.1.

**C. Admin MCP server** — warstwa zarządzania. Tools i resources z sekcji 5.2. Pisze konfigurację, którą czyta ingest (A), i może wywoływać akcje runtime (start/stop/backfill) na procesie A.

Przepływ danych:
- Operator → C (ustawia relays, subskrypcje) → A (czyta config, łączy się z relays, ingestuje) → graf Neo4j → B ← klient LLM (zapytania).
- C i B nie komunikują się ze sobą. A i C rozmawiają przez stan w Neo4j (+ ewentualnie sygnały IPC dla start/stop).

Podział pozwala:
- Testować B na statycznej bazie bez ingestu.
- Używać ingestu niezależnie (przez C albo bez C, z ręcznym configiem).
- Oddzielić uprawnienia read/write (patrz 5.2).

---

## 7. Zakres wiedzy, którą ZAKŁADAMY po stronie klienta

Dotyczy **client MCP servera** (5.1). Admin MCP (5.2) działa inaczej — jego klient nie musi znać Nostr, tylko interpretować wysokopoziomowe tools zarządzające.

Klient client MCP (LLM asystenta) przychodzi z:
- Znajomością protokołu Nostr i NIP-ów (publicznie udokumentowane, w danych treningowych większości modeli).
- Znajomością znaczenia popularnych kindów (0, 1, 3, 7, 30023, itd.).
- Znajomością konwencji tagów (`e`, `p`, `a`, `r`, `t`, `d`, markerów jak "reply"/"root", itd.).
- Znajomością Cypher i Pythona.

Od serwera klient dostaje:
- Opis, **jak ta wiedza jest odwzorowana w naszym konkretnym grafie** (resource `nostr://schema/graph`).
- Możliwość wykonania zapytania (tools `run_cypher`, `run_python`).

---

## 8. Decyzje początkowe i otwarte pytania

Po wprowadzeniu admin MCP większość wcześniejszych "otwartych pytań" to już **seed state** — konfiguracja startowa, którą potem modyfikuje się przez admin MCP bez zmian w kodzie.

### Seed state (propozycja)

1. **Kindy:** `0` (profile), `1` (notes), `3` (contacts), `7` (reactions). Opcjonalnie: `6` (reposts), `30023` (long-form). Kind 4 (DM-y) odrzucony — zaszyfrowane, bezużyteczne w grafie.
2. **Relays:** 2–3 popularne (`wss://relay.damus.io`, `wss://nos.lol`, `wss://relay.nostr.band`).
3. **Tryb:** najpierw backfill N ostatnich eventów, potem live subscription. Operator może zawęzić przez admin MCP po starcie.
4. **Kind 5 (deletions):** zgodnie z filozofią kind-agnostic — zapisujemy jak każdy inny event.

### Otwarte pytania architektoniczne

1. **Storage konfiguracji:** Neo4j (`:Config:*`) vs sidecar (JSON/SQLite). Propozycja: Neo4j — jeden storage, łatwy dump/restore.
2. **Komunikacja admin MCP ↔ ingest dla akcji runtime** (`start_ingest`, `stop_ingest`, `backfill`): sygnał Unix, plik-trigger, gniazdo, REST na localhost? Jeśli ingest poll'uje config co N sekund, część akcji (start/stop subskrypcji) można realizować przez samą zmianę configu — wtedy IPC jest potrzebne tylko do backfillu i twardego restartu.
3. **Reload konfiguracji:** poll interval vs change feed Neo4j. Poll prostszy, wystarczy dla prototypu.

---

## 9. Zasady przekrojowe (nie powtórzone wyżej)

- **Nazwy wszystkich węzłów, relacji, properties — po angielsku.**
- **Uniwersalność ponad wydajność i bezpieczeństwo** — to projekt prototypowy, lokalny.
- **Separacja read/write przez osobne serwery MCP i osobne credentials Neo4j** — client nigdy nie modyfikuje stanu, admin nie odpytuje danych przez Cypher (tylko przez wysokopoziomowe tools).

---

## 10. Następne kroki

Po otwarciu nowej rozmowy:

1. Potwierdzić decyzje z tego dokumentu (lub zmienić, jeśli coś się przemyślało).
2. Odpowiedzieć na otwarte pytania z sekcji 8 (storage konfiguracji, IPC ingest ↔ admin MCP, reload).
3. Postawić Neo4j w Dockerze (docker-compose, BEZ APOC). Utworzyć trzy role/usera: `ingest`, `client_mcp`, `admin_mcp` — patrz 5.2.
4. Zdefiniować schemat `:Config:*` (Relay, Subscription) i minimalny seed (sekcja 8).
5. Napisać ingest (pynostr → sterownik Neo4j) — czyta `:Config:*` z Neo4j, mapuje eventy według reguł sekcji 4.
6. Napisać **Client MCP server** (SDK MCP Python) wg 5.1:
   - tools: `run_cypher`, `run_python`
   - resource: `nostr://schema/graph` (treść = zawartość sekcji 3+4 w formie zjadliwej dla LLM)
7. Napisać **Admin MCP server** (SDK MCP Python) wg 5.2:
   - tools: add/remove/list dla relays i subskrypcji, `status`, `stats`, kontrola procesu ingestu
   - resources: `nostr://admin/config`, `nostr://admin/status`
8. Przetestować end-to-end: podłączyć klienta MCP (np. Claude Desktop) do obu serwerów, dodać relay + subskrypcję przez admin, zapytać o dane przez client.
