Skip to main content
Glama

personal-vector-mcp

Semantic search over your own markdown notes, run entirely on your machine and exposed to Claude Code as an MCP tool. A local Qwen3-Embedding-0.6B service turns notes into vectors, Qdrant stores them, and an MCP server lets Claude search them and answer with sources. No API key is needed, and no embedding or chat model is paid for: Claude Code is the step that writes the answer.

What's here

  • embedder/ is a FastAPI service that serves Qwen3-Embedding-0.6B (1024 dimensions) over an OpenAI-style /v1/embeddings endpoint.

  • mcp/ is an MCP server over streamable HTTP with two tools, search_notes and list_tags.

  • docker-compose.yml runs Qdrant, the embedder, and the MCP server together.

  • ingest.py splits notes into chunks, embeds them, and stores them in Qdrant.

  • query.py searches the stored chunks from the command line.

  • embed_client.py is the shared embedding client the host scripts use.

  • smoke.py, ingest_check.py, and mcp/check.py are the runnable checks.

Related MCP server: knowledgebased

Requirements

  • Docker with at least 6 GB of memory available to containers. The embedder alone uses about 3 GB once the model is loaded and more while it embeds a batch.

  • uv to run the host scripts.

  • Claude Code to ask questions through the MCP tool.

Everything runs on CPU. The embedder image is about 4 GB because the model is baked in at build time.

Add your notes

Put your notes in a data/ folder at the repo root. The ingest script picks up every *.md and *.txt file in it, including subfolders, and splits each into chunks of at most 1500 characters at paragraph, line, or word breaks. A frontmatter block at the top of a note is read for tags and a topic, as described under "Tag your notes", and is not chunked. Each search result names the file it came from, so descriptive filenames make better citations.

uv run ingest.py embeds each note under data/ that is not in data/ingested/, then moves it into data/ingested/, keeping its subfolder. So each run embeds only the notes added since the last one. To update a note, drop the new version into data/ again, and the next run replaces its old chunks and its old file. Edits made in place under data/ingested/, and files deleted from it, are only picked up by uv run ingest.py --rebuild. --rebuild drops the collection and re-ingests every note, data/ingested/ included. make ingest and make ingest-rebuild run the same two commands. If a run fails partway, the notes it finished are already in data/ingested/ and the next run picks up the rest.

Tag your notes

A note can start with a YAML frontmatter block, and the ingest script reads three optional fields from it:

---
tags: [cooking, food, korean cuisine]
topic: Author James Jones shares fusion Asian American recipes he formed after his trip through Southeast Asia
document_date: 2019-05-01
---

tags is a list of keywords that search results can be narrowed to. They are normalised on the way in: lowercased, trimmed, inner whitespace collapsed to one space, and duplicates dropped, so Korean Cuisine and korean cuisine are the same tag. topic is a one-sentence description of the whole note. It is embedded together with every chunk of the note, so a chunk is also found by what its document is about, while the stored text stays the chunk alone. document_date is when the note or its source was written, in any form YAML accepts. Other fields are ignored, and a note without frontmatter ingests as before. A frontmatter block that is not valid YAML stops the run with the file named, and the note stays in data/ for a fix.

Each stored chunk carries tags, topic, document_date, and date_ingested, the UTC time the ingest run stored it. Chunks stored before this existed carry none of these until uv run ingest.py --rebuild; searches still return them, with empty tags and null fields.

Run

Start Qdrant, the embedder, and the MCP server with docker compose up --build -d. The first build downloads the 1.2 GB embedding model once and bakes it into the image. Check the embedder with uv run smoke.py, which prints smoke ok and client ok. Ingest your notes with uv run ingest.py. Embedding takes minutes on CPU, about ten for a thousand chunks, so the first run is the slow one. Check the ingest itself with uv run ingest_check.py, which prints ingest ok and uses its own collection and temporary notes. Query the collection with uv run query.py "your question", which prints the five closest chunks with their scores and files. Check the MCP server with uv run mcp/check.py, which prints mcp ok. It searches your notes without assuming what they contain, so run it after ingest.py. The Qdrant dashboard is at http://localhost:6333/dashboard.

Each service restarts by itself when it crashes or Docker restarts, unless you stopped it with docker compose stop. After a reboot the stack comes back once Docker Desktop is running, so turn on its "Start Docker Desktop when you sign in" setting to make that automatic. While the embedder loads its model, for about a minute after Docker starts, search_notes reports that the embedder failed.

Ask Claude Code

The notes MCP server is registered in .mcp.json. The first time you start claude in this directory, it asks whether to use that server; approve it. If you declined by mistake, run claude mcp reset-project-choices and start claude again. claude mcp list shows notes as connected once the stack is up and the server is approved. Then ask Claude Code anything your notes cover, and it calls search_notes and cites the files it found.

search_notes takes a query, an optional k, the number of chunks to return, and an optional tags list. k defaults to 5 and is clamped to 1 to 20. With tags, only chunks carrying at least one of those tags are searched, with the same normalisation as ingest, so case and spacing do not matter. Each result carries the chunk's text, source, score, tags, topic, document date, and ingest time. list_tags returns every tag in the collection with the number of chunks carrying it, so Claude only filters on tags that exist. Its description tells Claude to search unfiltered first and narrow by tag only when you name a domain or the first results are off-topic. Both tools search the notes collection that ingest.py builds.

Configuration

Every service URL comes from the environment, and only the defaults mention localhost.

Variable

Default

Read by

EMBED_URL

http://localhost:8080

host scripts, MCP server

QDRANT_URL

http://localhost:6333

host scripts, MCP server

COLLECTION

notes

ingest.py, query.py, MCP server

MCP_URL

http://localhost:8000/mcp

mcp/check.py

The ingest and query scripts also load these from a .env file at the repo root. Inside compose, the MCP server gets http://embedder:8080 and http://qdrant:6333 from docker-compose.yml.

Embedding API

POST /v1/embeddings takes {"input": "text" or ["text", ...], "input_type": "document" or "query"} and returns OpenAI-shaped embeddings. input_type defaults to "document"; "query" applies the model's query prompt, which improves retrieval for questions. Empty input returns 422, and inputs longer than 2048 tokens are truncated. Clients should send at most 16 texts of up to 1500 characters per request; larger requests can exhaust the container's memory. GET /health reports the model and its dimension once it is loaded.

For example: curl -s localhost:8080/v1/embeddings -H 'Content-Type: application/json' -d '{"input": "hello", "input_type": "query"}'.

Offline and security

No script needs an API key or a network connection once the images are built and the Python dependencies are installed. Only Claude Code itself talks to Anthropic.

Qdrant, the embedder, and the MCP server have no authentication. Their ports (6333, 6334, 8080, 8000) are bound to 127.0.0.1, so only this machine can reach them. The MCP server also rejects requests whose Host header is not localhost, which blocks DNS-rebinding attacks from web pages. Add authentication before exposing any of them on a network.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables semantic search over local notes and documents using natural language queries. Supports multiple file types (Markdown, Python, HTML, JSON, CSV, text) with fast local embeddings and persistent ChromaDB vector storage.
    1
    -
  • A
    license
    A
    quality
    D
    maintenance
    Provides semantic search and a tag-based knowledge graph for any project, auto-discovering local markdown knowledge bases with YAML frontmatter.
    10
    3 npm
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI agents to search local Markdown documents using natural language, with automatic indexing and section-level retrieval.
    10
    6 npm
    1
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables semantic search over local markdown documentation by indexing files and ranking results using vector similarity and BM25 fusion.
    1
    2 npm
    Apache 2.0