vkb

inkiebeard/galactic-vkb
0 starsCommunity

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

A local-first semantic memory system that enables document ingestion, semantic querying, and knowledge graph traversal via MCP.

README.md

vkb — Vector Knowledge Base

A self-contained, local-first semantic memory system built on Postgres + pgvector and Ollama. Exposes a full Model Context Protocol (MCP) server so any MCP-capable host (Claude Desktop, Claude CLI, etc.) can ingest documents, query semantically, and traverse a knowledge graph.

---

Architecture overview

graph TD
    subgraph Clients
        A[MCP Host\nClaude Desktop / CLI]
        B[Browser UI\nlocalhost:4242]
    end

    subgraph vkb process
        MCP[MCP Server\nstdio or HTTP Streamable :3333]
        HTTP[HTTP Server\n:4242]
        COORD[Coordinator\nWorker Pool]

        subgraph Workers
            IW[ingest-worker × N]
            FW[finetune-worker]
            RW[retune-worker]
        end

        subgraph Pipelines
            IPIPE[Ingest Pipeline]
            FPIPE[Finetune Pipeline]
            RPIPE[Retune Pipeline]
        end

        subgraph Adapters
            FETCH[fetch\nreadability / pdf / epub]
            CHUNK[chunk\nsliding-window]
            EMBED[embed\nollama]
            SECT[section\nsimilarity-valley]
            REL[relation\nheuristic + LLM]
            LLM[llm\nollama]
            RAW[rawstore\nfilesystem or s3]
        end
    end

    subgraph External
        PG[(Postgres\npgvector)]
        OLL[Ollama]
        FS[Filesystem / S3\nrawstore/]
    end

    A -->|MCP tools| MCP
    B -->|REST / WebSocket| HTTP
    MCP --> COORD
    HTTP --> COORD
    COORD --> IW & FW & RW
    IW --> IPIPE
    FW --> FPIPE
    RW --> RPIPE
    IPIPE & FPIPE & RPIPE --> FETCH & CHUNK & EMBED & SECT & REL & LLM & RAW
    EMBED -->|vectors| OLL
    LLM --> OLL
    RAW --> FS
    IPIPE & FPIPE & RPIPE -->|upsert| PG

---

Ingestion pipeline

flowchart TD
    START([Job queued]) --> DEDUP

    DEDUP{Content hash\nalready known?}
    DEDUP -- yes, unchanged --> SKIP([Skip — existing entity returned])
    DEDUP -- no / changed --> FETCH

    subgraph FETCH["fetching — Persist"]
        FETCH2[Fetch raw text\nURL → readability / PDF / EPUB\nor staging file]
        FETCH2 --> HASH[Compute SHA-256\ncontent hash]
        HASH --> WRITE[Write entity.md + chunks.ndjson\nto RawStore]
        WRITE --> F_COMMIT[Commit raw_store_key\nto DB ✓]
    end

    F_COMMIT --> CHUNK_S

    subgraph CHUNK_S["chunking"]
        CHUNK2[Sliding-window chunk\nwrite chunk rows to DB]
    end

    CHUNK_S --> EMBED_S

    subgraph EMBED_S["embedding"]
        EMBED2[Embed each chunk\nvia Ollama]
    end

    EMBED_S --> SECT_S

    subgraph SECT_S["sectioning"]
        SECT2[Group chunks into sections\nsimilarity-valley or positional]
    end

    SECT_S --> SUM_S

    subgraph SUM_S["summarising"]
        SUM_C[Summarise each chunk — LLM]
        SUM_SEC[Summarise each section — LLM]
        SUM_E[Summarise entity — LLM]
    end

    SUM_S --> LINK_S

    subgraph LINK_S["linking"]
        REL_H[Heuristic relation extraction]
        REL_L[LLM relation extraction\noptional]
    end

    LINK_S --> TAG_S

    subgraph TAG_S["tagging"]
        TAG[Extract tags from meta\nAssert tag:* relations\nbetween co-tagged entities]
    end

    TAG_S --> DONE([Entity ready — status = ready])

Finetune pipeline

The finetune pipeline enriches already-ingested entities without re-chunking or re-embedding. It runs as a separate finetune-worker process and is triggered via vkb_finetune (MCP) or POST /finetune (HTTP).

flowchart TD
    START([Finetune job queued]) --> EX

    subgraph EX["extracting"]
        TOP[Find top-N nearest neighbours\nvia embedding similarity]
        TOP --> LLM_REL[LLM relation extractor\nupsert content_llm relations]
    end

    EX --> TAG

    subgraph TAG["tagging"]
        LLM_TAG[LLM keyword tagger\nmerge tags into meta.tags]
        LLM_TAG --> TAG_REL[Assert tag:* relations\nbetween co-tagged entities]
    end

    TAG --> DONE([Finetune complete])

---

Requirements

| Tool | Version | |---|---| | Node.js | ≥ 22 | | Docker + Docker Compose | any recent | | Ollama | any recent |

Pull the models vkb uses by default:

ollama pull nomic-embed-text
ollama pull gemma4:e4b

---

Quick start

# 1. Clone and install
git clone <repo-url> galactic-vkb
cd galactic-vkb
npm install

# 2. Configure
cp .env.example .env
# Edit .env if needed — defaults work out of the box with Docker Compose

# 3. Start Postgres (pgvector-enabled)
npm run db:up

# 4. Run migrations
npm run migrate

# 5. Start vkb
npm run dev          # development (tsx watch)
# or
npm run build && npm start   # production

The UI is available at http://localhost:4242/ once running. Navigate between the graph view (#viz) and ingest form (#ingest) using the header tabs.

---

Adding vkb as an MCP server

vkb uses stdio transport when MCP_PORT is 0. The MCP server is also accessible over HTTP Streamable (MCP 2025-03-26 spec) when MCP_PORT > 0 (default 3333), with session management for multi-client use.

Claude Desktop (claude_desktop_config.json)

Open (or create) ~/.claude/claude_desktop_config.json and add an entry under mcpServers:

{
  "mcpServers": {
    "vkb": {
      "command": "node",
      "args": ["/absolute/path/to/galactic-vkb/dist/index.js"],
      "env": {
        "DATABASE_URL": "postgres://vkb:vkb@localhost:5433/vkb",
        "OLLAMA_BASE_URL": "http://localhost:11434",
        "EMBED_MODEL": "nomic-embed-text",
        "LLM_MODEL": "gemma4:e4b",
        "MCP_PORT": "0"
      }
    }
  }
}

Tip: Run npm run build first so dist/index.js exists. MCP_PORT=0 forces stdio mode — no HTTP server is started.

For development (no build step), use tsx instead:

{
  "mcpServers": {
    "vkb": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/galactic-vkb/src/index.ts"],
      "env": {
        "DATABASE_URL": "postgres://vkb:vkb@localhost:5433/vkb",
        "OLLAMA_BASE_URL": "http://localhost:11434",
        "MCP_PORT": "0"
      }
    }
  }
}

Claude CLI (inline)

MCP_PORT=0 DATABASE_URL=postgres://vkb:vkb@localhost:5433/vkb \
  claude --mcp-server "vkb:node /absolute/path/to/galactic-vkb/dist/index.js"

---

Available MCP tools

| Tool | Description | |---|---| | vkb_ingest | Submit text, a URL, or a file path for ingestion. Optional source_context (external\|conversation\|self_authored) and meta object. Inline text is deduplicated by SHA-256 before queuing. | | vkb_ingest_bulk | Submit up to 200 items in a single call. Each item has the same shape as vkb_ingest. Deduplication is applied per-item; unchanged items are returned with skipped: true. | | vkb_job | Poll a background job by ID. Returns stage, progress counters, entity_id, kind, and error_detail on failure. | | vkb_query | Semantic search across all ingested content. Supports k, type, threshold (float 0–1), and include_sections. Returns an actionable hint when results are empty. | | vkb_get | Fetch an entity or chunk by ID (kind: entity\|chunk). Entity responses include chunk IDs, sections, relations, and tag_context (co-tagged entities). | | vkb_raw | Read the raw stored text for an entity or chunk from the RawStore. | | vkb_relate | Assert an explicit relation between any two entity or chunk IDs. weight is auto-computed from cosine similarity if omitted. Asserted relations are never pruned. | | vkb_neighbors | Retrieve an N-hop relation subgraph from a seed node. | | vkb_delete | Delete an entity and all its data (chunks, sections, relations, RawStore files). Non-reversible. | | vkb_finetune | Queue a finetune job: LLM relation extraction + LLM keyword tagging. No re-chunking or re-embedding. Accepts optional entity_ids array or scope (entity type filter). | | vkb_retune | Trigger a re-embedding / relation refresh sweep immediately. force: true reprocesses all chunks regardless of embed model. | | vkb_status | Full system snapshot (entity/chunk/relation counts, queue depth, worker state, config). | | vkb_migrate | Run all pending SQL migrations. Idempotent. |

vkb_relate — the feedback loop tool

"The vkb_relate tool is underrated. As Claude works with your data and draws connections, you can have it assert new relations back into the graph. Over time Claude becomes a contributor to the knowledge base, not just a consumer. That's a genuinely interesting feedback loop."

Every relation asserted via vkb_relate is marked origin: asserted — it is never pruned by retune sweeps and carries confidence: 1.0. The optional weight parameter (float 0–1) lets you express relative strength; if omitted it is computed automatically from the cosine similarity between the two nodes. This makes Claude's synthesis durable: connections it draws during a session persist and become first-class edges that future queries and vkb_neighbors traversals can follow.

Typical pattern: ```

1. Query for relevant chunks

vkb_query { text: "transformer attention mechanism" }

2. Identify a cross-document insight, then assert it

vkb_relate { source_id: "<chunk-A>", target_id: "<chunk-B>", rel_type: "shares_mechanism_with" }

3. Traverse what's grown

vkb_neighbors { id: "<chunk-A>", hops: 2 } ```

You can also use vkb_finetune to have the LLM automatically extract relations and keyword tags across a set of entities — a useful complement to explicit vkb_relate calls when working with a large corpus.

vkb_neighbors — N-hop subgraph retrieval

Walks the relation graph outward from a seed node up to hops steps (default 2, max 5). Returns:

  • nodes — every reachable entity or chunk, annotated with kind, hop distance from the seed, and its summary.
  • edges — all relations between discovered nodes (not just the traversal path), enabling local graph rendering or further reasoning.

| Parameter | Default | Description | |---|---|---| | id | required | Seed entity or chunk UUID | | hops | 2 | Traversal depth (1–5) | | min_confidence | 0.0 | Skip edges below this confidence | | rel_type | — | Only follow edges of this type | | max_nodes | 50 | Cap on total nodes returned |

---

HTTP API

The observability server runs on OBS_PORT (default 4242) and exposes both the SPA and a REST API.

All responses follow the envelope { ok: true, data: … } / { ok: false, error: "…" }. When Postgres is unreachable, routes that need the DB return 503 with "Database unavailable".

Authentication

Set OBS_SECRET=your-secret in .env. When set, all API requests must include:

Authorization: Bearer your-secret

Health

GET /health

Returns the live status of each dependency:

{ "ok": true, "uptime": 42.3, "postgres": true, "ollama": true }

Returns 503 with "postgres": false when Postgres is unreachable.

Ingest

# URL
curl -X POST http://localhost:4242/ingest \
  -H "Content-Type: application/json" \
  -d '{ "type": "url", "ref": "https://example.com/article" }'

# Local file (supports .md, .txt, .pdf, .epub, .yaml, .json)
curl -X POST http://localhost:4242/ingest \
  -H "Content-Type: application/json" \
  -d '{ "type": "doc", "ref": "/absolute/path/to/file.md" }'

# Inline text
curl -X POST http://localhost:4242/ingest \
  -H "Content-Type: application/json" \
  -d '{ "type": "note", "text": "Some text…", "source_context": "conversation", "meta": { "tags": ["example"] } }'

Response:

{ "ok": true, "data": { "job_id": "3f2a1b4c-…", "entity_id": "9d8e7f6a-…" } }

Ingestion is asynchronous — poll /jobs for completion. Inline text is deduplicated by SHA-256 content hash; if identical content already exists as a ready entity, the job is skipped and the existing entity is returned.

Re-ingest

# Re-run pipeline for a single entity (must have stored raw content)
curl -X POST http://localhost:4242/reingest \
  -H "Content-Type: application/json" \
  -d '{ "entity_id": "9d8e7f6a-…" }'

# Re-queue all entities that have stored raw content
curl -X POST http://localhost:4242/reingest \
  -H "Content-Type: application/json" \
  -d '{}'

Finetune

Runs LLM relation extraction and keyword tagging on already-ingested entities without re-chunking or re-embedding:

# Finetune specific entities
curl -X POST http://localhost:4242/finetune \
  -H "Content-Type: application/json" \
  -d '{ "entity_ids": ["9d8e7f6a-…"] }'

# Finetune all entities of a given type
curl -X POST http://localhost:4242/finetune \
  -H "Content-Type: application/json" \
  -d '{ "scope": "url" }'

Query

curl -X POST http://localhost:4242/query \
  -H "Content-Type: application/json" \
  -d '{ "text": "How does quantum entanglement work?", "k": 5, "threshold": 0.7 }'

Retune

Starts a background retune sweep (re-embeds stale chunks, prunes weak relations):

curl -X POST http://localhost:4242/retune \
  -H "Content-Type: application/json" \
  -d '{ "scope": "all", "force": false }'

Jobs

GET /jobs?kind=ingest&stage=queued&limit=50

| Query param | Values | Description | |---|---|---| | kind | ingest, retune, finetune | Filter by job type | | stage | queued, fetching, chunking, embedding, sectioning, summarising, linking, tagging, extracting, done, error | Filter by stage | | limit | 1–200 (default 50) | Max results |

Results include the entity ref and meta for context.

Entities

GET  /entities?type=url&status=ready&source_context=external&q=quantum&id=<uuid>&from=2025-01-01&limit=50&offset=0
GET  /entities/broken
GET  /entities/projection?offset=0&limit=500
GET  /entities/:id
GET  /entities/:id/raw
DELETE /entities/:id
POST /entities/bulk-action

/entities/broken returns non-ready entities annotated with their latest job and a remediation hint:

  • reingest — raw content is available, pipeline can be re-run
  • no_raw — source content was not persisted; manual intervention needed
  • stuck — an active job exists but has not progressed

/entities/projection returns a paginated UMAP 3D projection (mean-pooled chunk embeddings per entity), used by the graph view. The projection is cached and recomputed in the background whenever an ingest job completes.

POST /entities/bulk-action applies an action to a list of entity IDs:

{ "ids": ["<uuid>", "…"], "action": "delete" | "reingest" | "reingest_force" | "finetune" }

Returns { results, succeeded, failed }.

Chunks

GET /chunks?entity_id=<uuid>&limit=500&offset=0
GET /chunks/projection?offset=0&limit=500
GET /chunks/:id
GET /chunks/:id/raw

/chunks/projection returns a paginated UMAP 3D projection of individual chunk embeddings (same cache/versioning as entity projection).

Relations

GET /relations?origin=heuristic&rel_type=related_to&min_confidence=0.7&limit=50

| Query param | Description | |---|---| | origin | content_heuristic, content_llm, semantic, asserted | | rel_type | Relation label string | | min_confidence | Float 0–1 | | min_weight | Float | | source_kind | entity or chunk | | limit | 1–50000 (default 50) |

Status

GET /status

Returns entity/chunk/relation counts, queue depths, worker state, index status, and active config.

---

WebSocket event stream

Connect to ws://localhost:4242/stream (or wss:// with TLS) to receive live pipeline events. Browsers that cannot send custom headers can authenticate via query param: ?token=<OBS_SECRET>.

| Event type | Payload fields | Description | |---|---|---| | stage_change | job_id, stage | Job moved to a new pipeline stage | | complete | job_id | Job finished successfully | | error | job_id, payload | Job failed — payload contains error detail | | heartbeat | job_id (empty), pid | Sent every ~10 s per worker; confirms the coordinator is alive | | worker_crash | name, code, signal, ts | A worker process exited unexpectedly and is being respawned | | projection_version | resolution (chunk\|entity), version, total, ts | UMAP projection recomputed after a job completes | | retune_scheduled | ts | Coordinator queued a periodic retune sweep | | db_unavailable | ts | Postgres connection lost — UI shows an alert banner | | db_available | ts | Postgres connection restored |

---

npm scripts

| Script | Description | |---|---| | npm run dev | Start with tsx watch (hot reload) | | npm run build | Compile TypeScript → dist/ | | npm start | Run compiled build | | npm run migrate | Apply database migrations | | npm run db:up | Start Dockerised Postgres | | npm run db:down | Stop containers | | npm run db:reset | Wipe volume and restart Postgres | | npm run pack:mcpb | Build and package as a .mcpb bundle |

---

Configuration

All settings are read from environment variables (or a .env file). Defaults are shown.

Infrastructure

| Variable | Default | Description | |---|---|---| | DATABASE_URL | postgres://localhost/vkb | Postgres connection string | | RAWSTORE_ADAPTER | filesystem | Raw content storage: filesystem or s3 | | RAWSTORE_PATH | ./rawstore | Root path for filesystem rawstore | | RAWSTORE_S3_BUCKET | — | S3 bucket name (when RAWSTORE_ADAPTER=s3) | | RAWSTORE_S3_ENDPOINT | — | S3-compatible endpoint URL (optional override) |

Ollama / models

| Variable | Default | Description | |---|---|---| | OLLAMA_BASE_URL | http://localhost:11434 | Ollama API base URL | | EMBED_MODEL | nomic-embed-text | Embedding model | | EMBED_DIM | 768 | Embedding dimension (must match model output) | | LLM_MODEL | gemma4:e4b | LLM model for relation extraction and summarisation | | LLM_RELATION_EXTRACTION | true | Use LLM to extract relations (set false to use heuristics only) | | LLM_EXTRACT_CANDIDATES | 20 | Candidate chunks the LLM considers per extraction pass |

Chunking & sectioning

| Variable | Default | Description | |---|---|---| | CHUNK_SIZE | 512 | Target chunk size in tokens | | CHUNK_OVERLAP | 64 | Overlap between adjacent chunks | | SECTION_STRATEGY | similarity_valley | Sectioning strategy: similarity_valley or positional | | SECTION_SPLIT_THRESHOLD | 0.65 | Cosine similarity drop that triggers a section boundary | | SECTION_WINDOW_SIZE | 5 | Sliding window size for valley detection | | SECTION_MAX_SIZE | 8 | Max chunks per section |

Relations

| Variable | Default | Description | |---|---|---| | RELATION_THRESHOLD | 0.75 | Minimum cosine similarity to create a relation | | RELATION_TOP_K | 10 | Nearest neighbours considered per chunk | | RELATION_CONFIDENCE_STEP | 0.05 | Increment applied on relation confirmation | | RELATION_TTL_DAYS | 30 | Days before unconfirmed relations are pruned | | RELATION_PRUNE_THRESHOLD | 0.6 | Confidence below which relations are pruned on retune |

Summarisation

| Variable | Default | Description | |---|---|---| | SUMMARY_CONCURRENCY | 4 | Parallel LLM calls during the summarising stage | | SUMMARY_MAX_INPUT_CHARS | 12000 | Max characters fed to the entity-level summary prompt (~3 k tokens) |

Vector index

| Variable | Default | Description | |---|---|---| | IVFFLAT_THRESHOLD | 1000 | Chunk count above which an ivfflat index is created/maintained | | IVFFLAT_LISTS | 100 | Number of ivfflat lists (tune alongside chunk count) |

Workers & jobs

| Variable | Default | Description | |---|---|---| | WORKER_CONCURRENCY | 2 | Number of ingest worker processes | | INGEST_MAX_RETRIES | 3 | Max times a failed job is re-queued | | RETUNE_INTERVAL_HOURS | 6 | How often the retune worker runs automatically (0 = disabled) | | RETUNE_SUMMARISE | false | Regenerate entity summaries during retune | | JOB_TTL_DAYS | 7 | Days before completed/failed jobs are expired |

Servers & security

| Variable | Default | Description | |---|---|---| | MCP_PORT | 3333 | MCP server port (0 = stdio mode only, no auth layer) | | OBS_PORT | 4242 | Observability HTTP/WebSocket server port | | OBS_SECRET | — | Bearer token for the REST/browser API on OBS_PORT (leave unset to disable) | | MCP_SECRET | — | Bearer token for the HTTP MCP endpoint on MCP_PORT (leave unset to disable) | | TLS_CERT | — | Path to TLS certificate file (enables HTTPS/WSS on all servers) | | TLS_KEY | — | Path to TLS private key file | | LOG_LEVEL | info | Log verbosity: debug, info, warn, error. Pass --debug at startup as a shorthand for LOG_LEVEL=debug — also enables full request/response logging on all HTTP endpoints and MCP tool call tracing |

Note — stdio mode has no auth layer. When MCP_PORT=0 the process communicates over its own stdin/stdout pipe; MCP_SECRET and OBS_SECRET have no effect on it. Only set MCP_SECRET when running in HTTP mode and exposing the port outside localhost — most MCP clients (including Claude Desktop) do not send an Authorization header, so setting MCP_SECRET in a Claude Desktop config will silently block all tool calls with 401 Unauthorized.

Custom prompts

Provide paths to YAML or plain-text files to override the built-in LLM prompts:

| Variable | Description | |---|---| | SUMMARY_PROMPT_FILE | Entity summary prompt | | CHUNK_SUMMARY_PROMPT_FILE | Per-chunk summary prompt | | SECTION_SUMMARY_PROMPT_FILE | Section summary prompt | | RELATION_EXTRACT_PROMPT_FILE | Relation extraction prompt |

---

Observability UI

The SPA at http://localhost:4242/ has two views, selectable from the header:

Graph (#viz)

  • Entity nodes coloured by status (ready / pending / error)
  • Edges coloured by origin (heuristic / semantic / LLM / asserted)
  • Click a node to inspect entity details, chunks, and relations
  • Confidence histogram for relations

Ingest (#ingest)

  • Submit URLs, local files, or inline text
  • Live job progress via WebSocket
  • Per-job stage tracker

A banner appears at the top of the UI if Postgres becomes unreachable while the server is running, and dismisses automatically on reconnection.

---

Database resilience

On startup vkb probes the Postgres connection up to 10 times (3-second intervals) before giving up. This means it tolerates the Docker container taking a few seconds to become ready after docker compose up.

While running:

  • HTTP routes that need the DB return 503 when the connection is lost
  • The coordinator broadcasts a db_unavailable WebSocket event on the first failed heartbeat
  • Ingest workers pause for 30 seconds between retries instead of the normal 2-second poll interval
  • A db_available event is broadcast and the UI banner clears automatically when connectivity is restored

---

TLS

Set TLS_CERT and TLS_KEY to paths of a certificate and private key to enable HTTPS and WSS on all servers. A self-signed cert for local development can be generated with:

scripts/gen-cert.ps1

Related MCP servers

Browse all →