Skip to main content
Glama
adborroto

semantic-search-mcp

by adborroto

semantic-search-mcp

CI Security npm node License: MIT

Eine winzige, eigenständige RAG-lite-Abrufmaschine: Sie indiziert Dateien auf der Festplatte und beantwortet die Frage „Was ist semantisch relevant für diese Abfrage?“ – nichts weiter. Sie ruft kein LLM auf und generiert keine Antworten. Sie liefert die relevantesten Textabschnitte (Datei, Zeile, Bewertung) zurück, sodass der Verbraucher – ein Mensch, ein Skript oder ein LLM über MCP – entscheiden kann, was damit geschehen soll.

Alles läuft lokal und offline nach dem ersten Durchlauf:

  • Embeddings: @huggingface/transformers mit Xenova/all-MiniLM-L6-v2 und int8-quantisierten Gewichten auf der CPU. Keine GPU, kein API-Schlüssel, keine Netzwerkaufrufe zur Abfragezeit.

  • Vektorspeicher: @lancedb/lancedb – eine eingebettete, dateibasierte Vektordatenbank. Kein Serverprozess, kein Docker.

  • Schnittstellen: eine CLI und ein stdio MCP-Server, sodass jeder MCP-fähige Agent (Claude Code, Cursor, Zed, …) direkt in Ihrem Korpus suchen kann.

Schnellstart

npm install -g @adborroto/semantic-search-mcp

semantic-search add ~/code/my-project      # add a folder to the corpus
semantic-search index                      # embed it (incremental on later runs)
semantic-search search "how does the retry logic work"

Das ist die gesamte Einrichtung. Es gibt keine Konfigurationsdatei, die von Hand geschrieben werden muss – add erstellt und verwaltet sie für Sie. Um es ohne Installation auszuprobieren:

npx @adborroto/semantic-search-mcp add ~/code/my-project

Hinweis zur Installationsgröße: ~950 MB Abhängigkeiten, plus ein ~25 MB großes Embedding-Modell, das bei der ersten Nutzung heruntergeladen wird. Fast alles davon sind native Binärdateien, die Sie auf dieser Ebene nicht vermeiden können – @lancedb/lancedb (~430 MB inklusive seiner plattformspezifischen Binärdatei) und die ONNX-Laufzeit (~300 MB, die Builds für jede Plattform in einem Paket ausliefert). Beide werden einmal zwischengespeichert; alles nach dem ersten Durchlauf ist offline.

Related MCP server: rag-retriever-mcp

Voraussetzungen

  • Node.js >= 22 (node:sqlite, das vom Fallback-Backend verwendet wird, ist erst ab Version 22 stabil).

  • ~950 MB Festplattenspeicher für Abhängigkeiten und ~25 MB für das Embedding-Modell, plus etwa 1–3 KB pro indiziertem Abschnitt.

  • Keine GPU, keine externen Dienste, kein Datenbankserver.

Warum „RAG-lite“

Eine vollständige RAG-Pipeline ist: Abschnitte abrufen → an ein LLM übergeben → LLM schreibt eine Antwort. Dieses Projekt stoppt bei Schritt eins. Das hält es einfach, schnell, kostengünstig im Betrieb und leicht nachvollziehbar – und es lässt sich sauber mit jedem LLM oder Agenten-Framework kombinieren, das Sie bereits verwenden, anstatt eine eigene, meinungsstarke Generierungsschicht mitzuliefern.

Den Korpus verwalten

semantic-search add ~/code/api ~/notes     # add one or more folders
semantic-search list                       # show what's configured
semantic-search remove api                 # by folder name...
semantic-search remove ~/notes             # ...or by path
semantic-search config                     # where config + index actually live

add überprüft, ob jeder Pfad ein echtes Verzeichnis ist, löst ihn in einen absoluten Pfad auf und überspringt Duplikate (einschließlich desselben Verzeichnisses, das über einen Symlink erreicht wird). remove entfernt auch die Abschnitte dieses Ordners aus dem Index, sodass sein Inhalt nicht mehr in den Ergebnissen erscheint – übergeben Sie --keep-index, wenn Sie ihn aus dem Korpus entfernen, aber durchsuchbar halten möchten.

Wo die Daten gespeichert werden

Konfiguration und Index folgen der XDG-Basisverzeichnisspezifikation, sodass sie Upgrades überstehen und von jeder Installationsmethode gemeinsam genutzt werden:

Was

Speicherort

Konfiguration

~/.config/semantic-search/config.json

Index + Modell-Cache

~/.local/share/semantic-search/

Überschreiben Sie alles mit SS_CONFIG_PATH, SS_INDEX_DIR, SS_MODEL_CACHE_DIR oder den Standardwerten XDG_CONFIG_HOME / XDG_DATA_HOME. SS_STORE_BACKEND=sqlite erzwingt das Fallback-Backend.

Der Index enthält den wörtlichen Text von allem, was Sie indiziert haben. Wenn Sie dies auf privaten Code richten, enthält ~/.local/share/semantic-search/ diesen Inhalt im Klartext. Übergeben Sie ihn niemals und hängen Sie ihn nicht an einen Fehlerbericht an.

Jede Option ist in src/config.js dokumentiert – Blockgröße, Ignorier-Muster, Modellname, Top-k, Parallelität. Das direkte Bearbeiten von config.json funktioniert dafür immer noch; add/remove bewahren alle Schlüssel, die sie nicht selbst besitzen.

Verwendung

Index

semantic-search index                      # all configured folders
semantic-search index ~/code/one-project   # just this folder, ignoring config
semantic-search index --force              # reprocess everything

Die Indizierung ist inkrementell: unveränderte Dateien werden anhand des Änderungsdatums übersprungen, Dateien, deren Inhalt sich nicht geändert hat (nur berührt), überspringen die erneute Einbettung, und von der Festplatte gelöschte Dateien werden aus dem Index entfernt. Nur das, was sich tatsächlich geändert hat, wird neu verarbeitet.

Bei mehreren konfigurierten Ordnern durchläuft index sie nacheinander mit einer pro-Ordner-Überschrift und einer kombinierten Gesamtsumme:

[1/3] my-api  /home/me/code/my-api  ─────────────────────────────
  ↺ indexed   src/auth/middleware.js  (8 chunks)
  2 indexed  1,203 skipped  16 chunks  4.1s

[2/3] my-app  /home/me/code/my-app  ─────────────────────────────
  ...

──────────────────────────────────────────────────────────────
total  5 indexed  3,891 skipped  0 deleted  41 chunks  12.3s

Jeder index <path>-Aufruf entfernt nur veraltete Einträge für Dateien unter diesem Pfad, sodass die Indizierung von Ordner B niemals die Einträge von Ordner A berührt.

Nützliche Flags: --max-files <n> stoppt nach N neuen Dateien (begrenzt den Speicher bei riesigen Korpora), --concurrency <n> legt die Parallelität fest, --verbose protokolliert jede Datei auf stderr.

Suche

semantic-search search "how does the retry logic work" -k 5

Gibt eine Tabelle mit Dateipfad, Zeilennummer, Bewertung und einer Textvorschau aus.

Der Abruf ist hybrid: Die Abfrage durchläuft zwei unabhängige Arme – eine Vektorsuche über die Embeddings und eine BM25-Volltextsuche über dieselben Abschnitte – und die beiden Rangfolgen werden mit Reciprocal Rank Fusion fusioniert. Die Arme versagen auf unterschiedliche Weise: Der Vektorarm übersieht exakte Bezeichner, Fehlerzeichenfolgen und Konfigurationsschlüssel, für die er keine semantische Handhabe hat; der lexikalische Arm übersieht Paraphrasen. Die Ausführung beider ist ein Recall-Fix, und die Fusion auf Rang statt auf Bewertung verhindert, dass eine unbegrenzte BM25-Bewertung die Kosinus-Ähnlichkeit übertönt.

Setzen Sie "hybridSearch": false in config.json für eine reine Vektorsuche und "rrfK", um die Rangglättungskonstante von RRF anzupassen (Standard 60, aus dem Paper).

Was indiziert wird

Richten Sie es auf einen Ordner und alles darin wird rekursiv indiziert. Es gibt keine Positivliste von „unterstützten“ Dateierweiterungen – .dart, .kt, .java, .tsx, .sql, .erb und alles andere Textuelle wird unverändert indiziert, wobei .pdf und .docx zuerst durch einen Parser laufen.

Vier Dinge sind ausgeschlossen:

  1. Was auch immer git ignoriert, wenn der Ordner ein Git-Repository ist. .gitignore wird auf jeder Ebene berücksichtigt, zusammen mit .git/info/exclude, Ihrer globalen Ausschlussdatei und Negationsmustern (!keep.this). Dies wird an git ls-files delegiert und nicht neu implementiert, sodass es genau mit git übereinstimmt – was bedeutet, dass generierte und zugekaufte Ausgaben, die Ihr Projekt bereits ignoriert, ohne eine zweite Liste aus dem Index bleiben.

  2. Ihre .indexignore-Regeln (siehe unten), für Inhalte, die committet sind, aber nicht durchsuchbar sein sollten – Testdaten, Snapshots, eine eingecheckte Secrets-Vorlage.

  3. Binärdateien, nach Erweiterung (Bilder, Archive, Schriftarten, kompilierte Objekte, Modellgewichte) und nach Inhalt – ein NUL-Byte in den ersten 4 KB bedeutet binär, dieselbe Heuristik, die grep -I verwendet. Dies ist eine Sicherheitsmaßnahme, um Nicht-Text-Bytes vom Tokenisierer fernzuhalten, kein Urteil darüber, was indiziert werden sollte.

  4. Dateien über 500.000 Bytes (maxFileSizeBytes), was die Hauptsicherung gegen eine generierte einzeilige Megabyte-Datei ist, die den Speicher erschöpft.

Symlinks werden übersprungen, nicht verfolgt, sodass ein in einem Ordner platzierter Link keine externen Inhalte in den Index ziehen kann.

Für Ordner, die keine Git-Repos sind, gibt es kein .gitignore, auf das man sich stützen kann, daher gilt dennoch eine kleine integrierte Liste (node_modules/, .git/, dist/, build/, coverage/, vendor/, …).

Um mehr auszuschließen, legen Sie eine gitignore-artige .indexignore an einem der beiden Orte ab:

  • innerhalb eines Ordners, den Sie indizieren – Muster sind relativ zu diesem Ordner;

  • neben Ihrer Konfiguration (~/.config/semantic-search/.indexignore) – gilt überall.

Siehe .indexignore.example für einen Ausgangspunkt, der iOS-, Android-, Flutter-, Ruby- und JVM-Build-Artefakte abdeckt.

MCP-Server

semantic-search mcp

Startet einen stdio-MCP-Server, der sechs Tools bereitstellt.

search(query, k?) – semantische Suche, gibt rohes JSON zurück:

[{ filePath, text, score, offset, startLine }, ...]

gather(query, k?, contextLines?) – gleiche Suche, zurückgegeben als ein einzelner formatierter Markdown-Block, bereit zum Einfügen in einen Kontextfenster:

### [1/5]  my-api  ·  src/auth/session.js  ·  line 42  ·  score 0.923
```
...chunk text...
```

contextLines (Standard 0) liest N zusätzliche Zeilen um jeden Abschnitt aus der Quelldatei – nützlich, wenn eine Abschnittsgrenze den benötigten Kontext abschneidet.

list_folders() – jeder konfigurierte Ordner mit seinem Namen und absoluten Pfad. Ein guter erster Aufruf, damit der Agent weiß, welcher Korpus existiert.

cat_file(filePath, startLine?, endLine?) – liest eine Datei anhand des absoluten Pfads, wie von search/gather zurückgegeben. Beschränkt auf die konfigurierten Ordner (siehe Sicherheit).

grep(pattern, folder?, fileGlob?, caseSensitive?, maxResults?) – wörtliche oder Regex-Suche über den Korpus, für den Fall, dass Sie exakte Übereinstimmungen anstelle von Ähnlichkeit benötigen. Gefiltert gegen die exakt gleiche Dateiliste, die der Indexierer indizieren würde, sodass git-ignorierte und .indexignore-Dateien nicht durch eine exakte Übereinstimmungssuche durchsickern können.

my-api  ·  src/auth/session.js:42  export function createSession(user) {

index(root?, force?, maxFiles?, concurrency?) – löst eine inkrementelle Neuindizierung aus, sodass ein Agent den Korpus aktualisieren kann, ohne ein Shell-Kommando auszuführen.

Alle Such-Tools teilen sich denselben Ranking- und Dateiauflösungscode wie die CLI; keines implementiert ihn neu.

Bei einem MCP-Client registrieren

Claude Code:

claude mcp add --scope user semantic-search -- semantic-search mcp
claude mcp list   # should show "✔ Connected"

Jeder Client, der eine JSON-Serverdefinition akzeptiert:

{
  "mcpServers": {
    "semantic-search": {
      "command": "semantic-search",
      "args": ["mcp"]
    }
  }
}

Bevorzugen Sie hier eine globale Installation gegenüber npx: Ein bloßes npx löst das Paket jedes Mal neu auf, wenn der Server startet, was die Startlatenz erhöht und Upgrades unangekündigt übernimmt. Wenn Sie npx verwenden, fixieren Sie die Version – npx -y @adborroto/semantic-search-mcp@0.1.0 mcp.

Neue MCP-Server werden normalerweise nur beim Start einer Sitzung erkannt. Starten Sie daher nach der Registrierung eine neue Sitzung.

Sicherheit

Dies ist ein lokales Einzelbenutzer-Tool mit einem einfachen Vertrauensmodell: Alles in einem konfigurierten Ordner ist für jeden MCP-Client lesbar, der den Server erreichen kann.

  • cat_file verweigert Pfade außerhalb der konfigurierten Ordner, löst zuerst Symlinks auf, sodass ein in einem Ordner platzierter Link nicht zur Flucht verwendet werden kann.

  • grep wird gegen dieselbe Dateiliste gefiltert, die der Indexierer erstellt – gits Ignorierregeln plus Ihre .indexignore – sodass Dateien, die absichtlich von der Indizierung ausgeschlossen wurden, nicht durch eine exakte Übereinstimmungssuche durchsickern.

  • Unterprozesse werden mit argv-Arrays (niemals einer Shell) gestartet, sodass Muster keine Befehle injizieren können.

Angesichts dessen: Richten Sie es nicht auf einen Korpus, den Sie nicht Ihrem LLM-Anbieter übergeben würden – Abschnitte werden an den Client zurückgegeben, der sie angefordert hat. Siehe SECURITY.md.

Wie es funktioniert

Dateierkennung

Die Regel lautet „indiziere alles unter dem Ordner“, und der einzige interessante Teil ist, was nicht indiziert werden soll. Anstatt gits Ignorier-Semantik neu zu implementieren – verschachtelte .gitignore-Dateien, Negationen, info/exclude, die globale Ausschlussdatei – wird ein Git-Root aufgezählt mit:

git ls-files -z --cached --others --exclude-standard

Verfolgte Dateien plus unverfolgte, aber nicht ignorierte, beschränkt auf das Verzeichnis, in dem es ausgeführt wird. Alles, was git ignoriert, fehlt konstruktionsbedingt. Nicht-Git-Ordner fallen auf einen einfachen rekursiven Durchlauf mit der integrierten Musterliste zurück.

Dieselbe Funktion unterstützt sowohl den Indexierer als auch das MCP-grep-Tool (src/ignoreRules.js). Das ist beabsichtigt: grep führt ein echtes grep -r aus, das fröhlich Treffer innerhalb von git-ignorierten Build-Ausgaben meldet, daher filtert es seine Ergebnisse gegen die eigene Dateiliste des Indexierers. Wenn die beiden ihre Regeln getrennt ableiten würden, würden sie auseinanderdriften, und die Ignorierliste würde aufhören, eine Grenze zu sein.

Hybrider Abruf

Eine Abfrage durchläuft zwei Arme parallel:

  • Vektor – bette die Abfrage ein, nimm die nächsten Nachbarn nach Kosinus-Distanz, ordne diese Liste dann mit einem kleinen lexikalischen Boost für Abschnitte neu, die die wörtlichen Abfragebegriffe enthalten.

  • Lexikalisch – BM25 über denselben Abschnittstext, über einen LanceDB-Volltextindex (das sqlite-Fallback berechnet BM25 in JS, da node:sqlite nicht garantiert FTS5 mitliefert).

Die beiden Rankings werden mit RRF fusioniert: jede Liste steuert 1 / (60 + rank) zu jedem Chunk bei, den sie zurückgibt, und die Beiträge werden summiert. Das Verschmelzen über den Rang anstelle der Bewertung ist der Punkt — Cosinus liegt in [-1, 1], während BM25 nach oben unbeschränkt ist, sodass das Addieren oder Mitteln der Rohwerte dazu führen kann, dass ein Arm den anderen je nach Korpusgröße stillschweigend überwältigt.

Warum überhaupt zwei Arme: Ein lexikalischer Boost, der auf die Ausgabe des Vektorarms angewendet wird, kann nur umordnen, was die Vektorabfrage bereits zurückgegeben hat. Ein Chunk, dessen einziges Signal eine exakte Term-Übereinstimmung ist — ein Fehlercode, ein Symbolname, ein Konfigurationsschlüssel ohne semantische Nachbarschaft — war unerreichbar, wenn er außerhalb des Vektorpools lag. Der lexikalische Arm ruft ihn unabhängig ab. Das ist eine Korrektur des Recall, nicht des Rerankings, und es ist der Grund, warum Suchbewertungen jetzt wie 0.03 statt 0.9 aussehen: Es sind RRF-Summen, keine Cosinus-Ähnlichkeiten. Nur ihre Reihenfolge ist sinnvoll.

Der Volltextindex wird am Ende jedes Indexierungslaufs neu aufgebaut, da ein FTS-Index keine Zeilen abdeckt, die nach seiner Erstellung hinzugefügt wurden — andernfalls wären die Chunks, die ein Lauf gerade geschrieben hat, für den lexikalischen Arm unsichtbar.

Chunking

Text wird in Absätze aufgeteilt und dann gierig in Chunks von etwa 200 Tokens mit ~35 Tokens Überlappung verpackt, gezählt mit dem echten Tokenizer des Einbettungsmodells anstelle einer Zeichenzählungsnäherung. Dies ist nicht willkürlich: all-MiniLM-L6-v2 hat ein Fenster von 256 Tokens und kürzt alles Längere stillschweigend ab, daher werden Chunks so bemessen, dass sie hineinpassen und Platz für die [CLS]/[SEP]-Tokens haben. Die Überlappung wird zusätzlich begrenzt, sodass Überlappung plus der nächste Absatz diese Grenze niemals überschreiten können — andernfalls würde das Ende eines Chunks zur Einbettungszeit abgeschnitten, während es von search noch zurückgegeben wird.

Ein einzelner Absatz, der größer als die harte Grenze ist (ein minifiziertes Bündel, eine riesige Logzeile), fällt auf eine Verpackung auf Wortebene mit derselben Überlappungslogik zurück, und jedes einzelne „Wort" über 500 Zeichen wird zuerst zerteilt, sodass nichts Riesiges jemals in einem Stück an den Tokenizer übergeben wird.

Tokenanzahlen werden einmal pro Absatz/Wort berechnet und zwischengespeichert, um bei der Überlappungsberechnung wiederverwendet zu werden. Eine frühere Version führte bei jeder Überlappungssuche eine erneute Tokenisierung durch, was bei kleinen Eingaben in Ordnung war, aber auf großen Repositories zu überhöhter CPU-Auslastung und Speicherwachstum im Multi-GB-Bereich führte. Wenn Sie den Chunker erweitern, bewahren Sie diese Eigenschaft.

Inkrementelle Neuindizierung

Es gibt kein separates Manifest — der Vektorspeicher ist das Manifest. Jeder gespeicherte Chunk trägt die mtimeMs seiner Quelldatei und einen sha256-Inhalts-Hash. Bei jedem Durchlauf:

  1. Wenn die mtime einer Datei auf der Festplatte mit dem gespeicherten übereinstimmt, überspringe sie, ohne die Datei zu lesen.

  2. Wenn sich die mtime geändert hat, aber der Inhalts-Hash identisch ist (ein touch), überspringe das erneute Einbetten.

  3. Andernfalls lösche die alten Chunks dieser Datei und füge neu eingebettete ein.

  4. Nach der Auflistung wird jeder indizierte Pfad, der nicht mehr auf der Festplatte vorhanden ist (und unter dem zu indizierenden Stammverzeichnis liegt), entfernt.

Speicher-Backends

Die Voreinstellung ist LanceDB: eingebettet, dateibasiert, echte Vektorsuche. Ein node:sqlite + Brute-Force-Cosinus-Fallback (src/store/sqliteFallbackStore.js) implementiert dieselbe Schnittstelle (src/store/vectorStore.js) für Umgebungen, in denen Lances native Bindung nicht geladen wird — sandboxed Container, ungewöhnliche Architekturen. Wechseln mit SS_STORE_BACKEND=sqlite.

Der Fallback führt einen vollständigen Tabellenscan pro Suche durch: in Ordnung für Zehntausende von Chunks, nicht darüber hinaus. Lances Standardmetrik ist L2, nicht Cosinus, daher setzt dieses Projekt explizit .distanceType('cosine') bei jeder Abfrage, da Einbettungen als normalisierte Vektoren verglichen werden.

Projektstruktur

src/
  config.js            Defaults + config file resolution (XDG) — the only source of tunables
  configFile.js        Read/modify/write the config file (backs add/remove/list)
  embeddings.js        transformers.js pipeline + tokenizer (lazy singletons)
  chunker.js           Token-aware paragraph packing with overlap
  ignoreRules.js       What is indexable: git ignore rules + .indexignore + binary filter,
                       shared by the indexer and grep so they can't drift apart
  safePath.js          Path confinement for the MCP file-reading tools
  version.js           Version read from package.json
  extractors/          text (anything not binary), pdf (pdf-parse), docx (mammoth)
  store/
    vectorStore.js        Storage interface + backend selector
    lancedbStore.js       LanceDB implementation (default)
    sqliteFallbackStore.js node:sqlite + manual cosine fallback
  indexer.js           List + extract + chunk + embed + incremental upsert/prune
  search.js            Hybrid retrieval: vector + BM25 arms fused with RRF — shared by CLI and MCP
  mcp-server.js        MCP stdio server: the six tools above
  index.js             CLI entrypoint (commander)
scripts/index-all.sh   Batched indexing for very large corpora on constrained hosts (Linux)

Entwicklung

git clone https://github.com/adborroto/semantic-search-mcp.git
cd semantic-search-mcp
npm install
npm test              # unit + end-to-end (node:test, no framework)
npm run test:unit     # skip the slow end-to-end test
npm run lint

Eine config.json im Checkout-Root hat Vorrang vor dem XDG-Speicherort, sodass Sie gegen einen Scratch-Korpus entwickeln können, ohne Ihre tatsächliche Einrichtung zu berühren. Tests schreiben immer in temporäre Verzeichnisse. Siehe CONTRIBUTING.md.

Außerhalb des Rahmens (bewusst)

  • Antwortgenerierung. Dies gibt Chunks zurück, keine Antworten. Füttern Sie sie selbst in ein LLM.

  • Reranking mit einem zweiten Modell. Hybrid-Retrieval plus RRF ist abhängigkeitsfrei und kommt dem größtenteils nahe — es ist jedoch kein Cross-Encoder-Reranker.

  • Eine Weboberfläche. Nur CLI und MCP.

  • Massive Korpusgrößen. Entwickelt für einen persönlichen oder teamgroßen Korpus von Dokumenten und Code — Zehntausende von Chunks, nicht Millionen. Beide Backends gehen von diesem Maßstab aus.

Lizenz

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Local-first RAG indexing and semantic search MCP server. Enables document retrieval and context-aware queries using local embedding models.
    3
    13 npm
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    A local-first document retrieval engine that mounts as an MCP tool for agents to index files, search for relevant passages, and let the agent's own LLM answer.
    4
    -
  • A
    license
    A
    quality
    B
    maintenance
    Indexes local Markdown/text files into a SQLite database with vector embeddings and provides MCP tools for semantic search without cloud dependencies.
    3
    AGPL 3.0