MCP server: One MCP server.

Local Vector Search MCP

@dimonsmartMCP server · External

Local Markdown search, navigation and editing via MCP, with optional vector embeddings.

Freeno charge per call

Runs on your machine from its package; the steps are below.

What it does

  1. Reads
  2. Writes
  3. High impact

At most: creates or changes data: records, files, settings.

What it costs

Free

Odel charges nothing per call. The publisher may charge for its own service.

Runs locally over stdio

This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).

NuGetDimonSmart.LocalVectorSearchMcpv0.1.3

Published to NuGet as `DimonSmart.LocalVectorSearchMcp` — run it with your .NET MCP host.

README20 min read

Give Codex, Claude Code, and ChatGPT a local, editable, project-aware Markdown workbench.

LocalVectorSearchMcp is a local MCP server that indexes one project's Markdown files into a project-local SQLite database. Agents can search, read focused semantic slices, edit by semantic pointer, manage Markdown files, navigate the workspace, and manage ordinary image assets.

It helps an agent find the right specification, architectural decision, guide, or invariant without loading the entire documentation set into its context.

Why use it?

Large repositories often contain the answer, but not in the file the agent happens to open first.

  • Exact text search finds identifiers, API names, and quoted terminology.
  • Vector search finds conceptually related documentation even when wording differs.
  • Hybrid search combines both result lists through Reciprocal Rank Fusion.
  • kb_read returns focused slices from the current Markdown source instead of forcing the agent to read whole files or wait for indexing.
  • Element-level optimistic concurrency prevents an agent from overwriting a semantic target that changed after it was read, while unrelated document edits do not block the patch.
  • Optional file watching keeps the index synchronized with edits from VS Code, Obsidian, or other editors.
  • PNG, JPEG, WebP, and GIF files can be stored as non-indexed assets throughout knowledgeBase.root (default images/).
  • Every project gets its own local SQLite index.
  • No cloud database or mandatory YAML configuration is required.

How it works

text
Workspace Markdown (source of truth)
       ↓
Markdown elements and search chunks
       ↓
SQLite FTS5 (+ sqlite-vec when embeddings are enabled)
       ↓
Scoped retrieval + semantic editing
       ↓
MCP tools over stdio

images/ assets
       ↓
save / list / load / delete / safe move
       ↓
not indexed

Claude Code and Codex can launch the stdio server directly. ChatGPT can use the same MCP surface through OpenAI Secure MCP Tunnel, where the external tunnel-client bridges ChatGPT to the local stdio process.

Install the .NET tool (requires .NET 10):

bash
dotnet tool install --global DimonSmart.LocalVectorSearchMcp

Alternatively, run a pinned published version directly with dnx (requires the .NET 10 SDK; no global tool install):

bash
dnx DimonSmart.LocalVectorSearchMcp@<version> --yes -- \
  --project-root "$PWD" --embedding-provider none --search-mode lexical --status

The official MCP Registry uses the canonical name io.github.DimonSmart/local-vector-search-mcp. A listing is available only after a matching NuGet release has been successfully registered.

To register the server using dnx without a global install:

bash
codex mcp add local-vector-search -- dnx DimonSmart.LocalVectorSearchMcp@<version> --yes -- \
  --project-root "$PWD" --embedding-provider none --search-mode lexical --watch-files

claude mcp add --transport stdio local-vector-search -- dnx DimonSmart.LocalVectorSearchMcp@<version> --yes -- \
  --project-root "$PWD" --embedding-provider none --search-mode lexical --watch-files

Replace <version> with a version published on NuGet.org, and run the commands from the Markdown project's root. The server uses stdio and offline lexical mode here; no Ollama endpoint or API key is required.

From the project you want to index, run the following. This needs no Ollama server, no remote embedding endpoint and no API key:

bash
local-vector-search-mcp --project-root "$PWD" \
  --embedding-provider none --search-mode lexical --reindex

Register a project-specific MCP server using the same arguments:

bash
codex mcp add local-vector-search -- \
  local-vector-search-mcp --project-root "$PWD" \
  --embedding-provider none --search-mode lexical --watch-files

The SQLite FTS5 index is stored in .local-vector-search-mcp/index.db under the project root. Markdown remains the source of truth. The server is read-only for Markdown and images unless knowledgeBase.allowWrites: true is explicitly set in YAML.

See the IDD integration example for .idd/intent, explicit paths, dnx, and Claude Code.

Optional hybrid/semantic quick start

1. Install the tool and embedding model

bash
dotnet tool install --global DimonSmart.LocalVectorSearchMcp
ollama pull bge-m3:latest

Make sure Ollama is running before the first reindex.

2. Connect your client

Run the command from the project whose documentation should be indexed.

Claude Code

bash
claude mcp add local-vector-search \
  --scope local \
  --transport stdio \
  -- local-vector-search-mcp

Codex

bash
codex mcp add local-vector-search \
  -- local-vector-search-mcp

ChatGPT

Keep LocalVectorSearchMcp as a local stdio server and bridge it with OpenAI Secure MCP Tunnel. For the tunnel scenario, use an explicit configuration with absolute knowledge-base and storage paths.

See ChatGPT via Secure MCP Tunnel.

3. Ask the agent to initialize and use the index

text
Check the local documentation index status. Reindex it if necessary,
then find the project's main architectural decisions and summarize them
with file references.

By default, the server indexes *.md files under the detected project root and stores the index in .local-vector-search-mcp/index.db.

See Getting started for the complete setup and verification flow.

Example tasks

text
Before changing the indexing pipeline, find the documented architectural
decisions and constraints that apply to it.
text
Search the project documentation for rules governing persisted state.
Summarize the relevant invariants and cite the source files.
text
Attach this PNG and save it as cover.png in the project images folder.
Return the Markdown image reference.

Capabilities

CapabilityImplementation
Exact searchSQLite FTS5 with BM25
Semantic searchsqlite-vec
Hybrid rankingReciprocal Rank Fusion
StorageProject-local SQLite
Indexed contentMarkdown
Editable contentMarkdown, guarded by exact element and subtree hashes
Image assetsPNG, JPEG, WebP, GIF; all image tools use root-relative paths, saving to images/ by default
NavigationFile listing, image listing, and heading outlines
TransportMCP over stdio
ClientsClaude Code, Codex, ChatGPT via OpenAI Secure MCP Tunnel
Default embeddingsLocal Ollama-compatible endpoint; use embedding.provider: none for offline FTS5-only mode

The MCP server exposes eighteen tools. MCP reindexing is asynchronous: kb_reindex starts or joins the single active reindex and returns immediately, while kb_status.indexing reports progress and the last outcome. The CLI --reindex command remains synchronous and exits only after indexing finishes.

  • kb_status — inspect the current index.
  • kb_reindex — start a background build or rebuild; monitor kb_status.indexing for progress and the final result.
  • kb_search — run optionally path-scoped lexical, semantic, or hybrid search against the derived index; each result identifies its indexed revision with indexedSourceHash.
  • kb_read — read the current Markdown source from a semantic pointer and receive the exact source revision sourceHash.
  • kb_patch — atomically replace/delete individual semantic elements or complete heading sections, and insert before/after elements.
  • kb_edit_table — source-aware editing of GFM table cells, rows, columns and alignment through a hashed table pointer.
  • kb_create, kb_move, kb_delete — manage Markdown source files.
  • kb_list_files — list Markdown files and non-indexed assets; use pathPrefix for a directory or exact file, and includeGlob for filename patterns.
  • kb_outline — return a deterministic heading tree.
  • kb_create_pdf_book — render a Markdown document as a formatted PDF book.
  • kb_download_file — return any permitted workspace file as a downloadable binary resource.
  • kb_save_image — save an image supplied through the OpenAI file parameter (optional targetPath).
  • kb_list_images — list supported image assets recursively with cursor pagination.
  • kb_load_image — return an existing image as a real MCP image content block.
  • kb_delete_image — delete one binary image under root without changing Markdown.
  • kb_move_image — safely move one binary image anywhere inside root without changing Markdown references.

For low-level MCP or connector calls, a tool with no user arguments still uses an explicit empty arguments object. The canonical kb_status wire call is:

json
{
  "name": "kb_status",
  "arguments": {}
}

Clients should not rely on omitting the argument object; {} is the interoperable contract for parameterless tools.

File-list filters

kb_list_files.pathPrefix is a root-relative directory path or an exact file path, not a partial filename prefix. books and books/ include files below books/; books/chapter.md selects that exact file; books/chapter does not match books/chapter.md. Use includeGlob for filename matching. Both filters apply when supplied.

GoalArguments
List a directory{ "request": { "pathPrefix": "books" } }
Select one file{ "request": { "pathPrefix": "books/chapter.md" } }
Find matching names{ "request": { "includeGlob": "qa-*.md" } }
Limit a pattern to a directory{ "request": { "pathPrefix": "books", "includeGlob": "**/*.md" } }

PDF book export

kb_create_pdf_book(path, outputPath?, pageFormat?, style?, profile?) creates a PDF from a root-relative Markdown file. It renders headings, emphasis, code, lists, quotes, links, GFM tables, and local images. The default output is the source name with a .pdf extension; it must remain under knowledgeBase.root. As a derived reading artifact, an existing PDF at that path is atomically replaced on the next export. Export requires knowledgeBase.allowWrites: true, does not alter Markdown, assets, or the search index, and returns {path,bytes,warnings}. Unavailable images are represented by a PDF placeholder and included in warnings; remote and absolute image URLs are not fetched. Formats: a4, a5 (default), a6, b5, letter; styles: compact (default), classic, modern, technical, academic.

Profiles (application-defined presets): screen = 96 DPI / JPEG quality 65, standard = 150 DPI / JPEG quality 82 (default), print = 300 DPI / JPEG quality 95. All profiles use PDF full compression, preserve Unicode text and the layout/physical size of images, and share identical embedded image resources. High-resolution JPEGs and PNGs may be downsampled; unchanged or already efficient files stay in their original encoding. JPEG with EXIF/ICC/Adobe metadata and PNG with explicit color metadata are preserved rather than risk altering orientation or colors; unsupported GIF/WebP retain the renderer's prior behavior. Output size is not guaranteed to fit kb_download_file's unchanged 25 MiB limit. Print is for ordinary printing, not PDF/X, CMYK production, or press-ready preflight. Image processing may take longer than before; the default profile may change the raster quality/size of previously exported PDFs.

Examples (all calls use the existing nested request):

json
{"request":{"path":"book.md","outputPath":"exports/screen.pdf","profile":"screen"}}
json
{"request":{"path":"book.md","outputPath":"exports/standard.pdf"}}
json
{"request":{"path":"book.md","outputPath":"exports/print.pdf","pageFormat":"a4","style":"classic","profile":"print"}}

Workspace file download

kb_download_file(path) returns the original bytes of one root-relative workspace file as an embedded MCP binary resource, followed by {path,mimeType,bytes,sha256} metadata. It works without write permission and supports any regular file up to 25 MiB; unknown extensions use application/octet-stream. Absolute/traversal paths, .git, internal index files, and symlink/junction/reparse-point paths are rejected. Use kb_load_image instead when an image preview or GIF-to-PNG conversion is needed.

Image assets

All five image tools work with PNG, JPEG, WebP and GIF files anywhere under knowledgeBase.root. Paths are root-relative; .git/, internal SQLite files, traversal and symlink/junction/reparse-point paths are forbidden. images/ is the recommended default folder, not an API restriction. Images are ordinary assets and are never indexed or embedded.

kb_save_image(file, fileName?, altText?, targetPath?) requires allowWrites=true and preserves the OpenAI openai/fileParams metadata. Upload or select an actual image in the client and pass the resulting file reference. Arbitrary text, raw Base64, and data URIs are not valid file values; if a client rejects them before invoking the tool, that client-side validation cannot be normalized by the server. Downloads remain HTTPS-only with SSRF/redirect protection, a 25 MiB streaming limit, MIME/signature checks and SHA-256. Without targetPath, saves default to images/, using fileName or source basename and numeric suffixes for collisions (cover.png, cover-2.png). Explicit targetPath includes the final file name and extension, may create intermediate safe directories, and never renames or overwrites an occupied destination (ALREADY_EXISTS). Combining targetPath and fileName is invalid.

The save result includes path, mimeType, bytes, lowercase sha256, and markdown. The generated Markdown destination is relative to the workspace root. For a document in chapters/, the caller must recompute and escape the relative destination from that document.

kb_list_images(cursor?, pageSize?) lists supported image extensions throughout the root, sorted case-insensitively with ordinal tie-breaking. Cursor pagination defaults to 50 items (range 1–200) and supports the last cursor item being deleted. kb_load_image(path) validates signature, extension and size; its MCP result contains an ImageContentBlock first and JSON metadata about the original file second, without StructuredContent. PNG/JPEG/WebP return their original bytes. GIF returns the first frame rendered as a PNG image block (image/png) because some ChatGPT connectors do not expose image/gif blocks; the text metadata still reports the source GIF's path, MIME type, byte count and SHA-256. The original GIF remains unchanged. The preview is static (no animation).

kb_move_image renames or moves only one binary image, anywhere under root. Input is {sourcePath,targetPath,expectedSha256?}. expectedSha256 is an optional 64-digit hex optimistic concurrency check; mismatches produce CONFLICT. Markdown links must be updated separately with kb_read and kb_patch. The response is {previousPath,path,sha256} and contains no Markdown/index fields.

kb_delete_image(path) simply deletes the supported binary file, without scanning for Markdown references, checking if it is used, or removing its parent directories. Its result remains {path,deleted:true}. Both binary move and delete can leave broken Markdown links. Image operations never schedule indexing.

To repair links, use separate kb_read / kb_patch calls on correctly addressable Markdown elements. A move-and-patch workflow is not a multi-file transaction: find relevant documents before the move, move the image, patch the necessary elements with current semantic anchors, and verify the new paths. Raw link constructs that cannot be addressed safely should not be rewritten by replacing arbitrary larger sections. Git can be used for recovery.

Saving, moving, and deleting require:

yaml
knowledgeBase:
  allowWrites: true

Expected tool errors return IsError=true and a JSON text object with code and message, e.g. INVALID_PATH, UNSUPPORTED_FORMAT, ALREADY_EXISTS, CONFLICT, PERMISSION_DENIED. Image tools have no indexSynchronized; for Markdown mutations its value is true only when reconciliation has already been confirmed or was unnecessary, and false when source changes succeeded but reconciliation is still unconfirmed.

Local-first and project-isolated

Each server process belongs to one project and one configured Markdown root. It cannot select or search another project's index through the MCP API.

The default embedding endpoint is local Ollama. Remote embedding endpoints are rejected unless they are explicitly enabled in YAML. Document text, embedding text, API keys, raw vectors, and signed image download URLs are not logged.

Documentation

Editable workspaces

Writes and external file watching are explicit opt-ins:

yaml
knowledgeBase:
  root: .
  allowWrites: true
  watchFiles: true

Markdown files remain the source of truth for indexed knowledge. kb_read and kb_outline parse the current source directly; kb_search uses the derived SQLite index and may temporarily return an older indexed revision. A Markdown mutation commits the source file first and then schedules synchronization of the derived SQLite index. The mutation call does not wait for reconciliation to finish; indexSynchronized: false means the source commit succeeded but derived-index synchronization has not yet been confirmed. Internal semantic pointers remain logical structural addresses. Public concrete-element pointers use logical~selfHash~subtreeHash, where both values are 16-character lowercase XxHash64 hashes over exact UTF-8 source. selfHash covers the element itself; heading subtreeHash covers the complete section source range owned by replace_section and delete_section, while leaf hashes are equal. kb_patch relocates only by kind plus selfHash; replace_section and delete_section additionally validate subtreeHash. sourceHash is still returned by kb_read for whole-file operations such as kb_move and kb_delete.

For structural edits, replace_element replaces exactly one Markdown element and delete removes exactly one element; deleting a heading with delete preserves its section body. replace_section replaces a heading together with its complete owned section subtree, while delete_section removes that whole subtree. Both section operations require a canonical v2 heading pointer and validate subtreeHash.

None of the five image tools changes Markdown or schedules Markdown index synchronization.

kb_patch examples

The MCP method accepts one request object. Inside it, operations is always an array, even when applying a single operation. New concrete-element pointers should use the canonical v2 anchors returned by kb_read, kb_search, or kb_outline. Legacy logical~selfHash anchors remain accepted for navigation and Self mutations, but replace_section requires the v2 subtree hash. kb_patch does not accept expectedSourceHash.

Use replace_element to replace exactly one editable Markdown element. For a heading, it changes only the heading itself and keeps the existing section body. The replacement must parse as exactly one editable element, and a heading replacement must keep the same heading level. Legacy replace remains supported as a deprecated alias with the same validation.

Use replace_section to rewrite a complete heading section atomically. The target must be a canonical v2 heading anchor with both selfHash and subtreeHash. The replaced range starts at that heading and continues through all nested content until the next heading with level less than or equal to the target level, or EOF. The replacement must start with a heading of the same level and may contain only deeper headings after it.

Replace one element:

json
{
  "request": {
    "path": "chapter.md",
    "operations": [
      {
        "kind": "replace_element",
        "pointer": "1.2.p2~8f41c721d904a8bc~8f41c721d904a8bc",
        "markdown": "New paragraph."
      }
    ]
  }
}

Replace a whole section:

json
{
  "request": {
    "path": "chapter.md",
    "operations": [
      {
        "kind": "replace_section",
        "pointer": "1.2~0123456789abcdef~fedcba9876543210",
        "markdown": "## Updated section\n\nNew body.\n\n### Nested heading\n\nNested body."
      }
    ]
  }
}

Insert after:

json
{
  "request": {
    "path": "chapter.md",
    "operations": [
      {
        "kind": "insert_after",
        "pointer": "1.2.p2~8f41c721d904a8bc~8f41c721d904a8bc",
        "markdown": "Additional paragraph."
      }
    ]
  }
}

Delete:

json
{
  "request": {
    "path": "chapter.md",
    "operations": [
      {
        "kind": "delete",
        "pointer": "1.2.p2~8f41c721d904a8bc~8f41c721d904a8bc"
      }
    ]
  }
}

Supported kind values are replace, replace_element, replace_section, delete_section, insert_before, insert_after, and delete. replace, replace_element, replace_section, and insert operations require markdown; delete does not. The special document pointer remains unhashed and can be used with insert_before or insert_after at document boundaries. Existing valid single-element replace requests remain compatible; multi-element replacement through replace is intentionally rejected and should use replace_section or other explicit operations.

An unrelated edit elsewhere in the file does not invalidate a Self mutation when the target selfHash is still valid. If the target moved because content was inserted above it, kb_patch relocates it only when the same element kind and exact selfHash identify exactly one current element. replace_section then also verifies the original subtreeHash, so edits anywhere in the owned section range—including raw/non-indexed Markdown—are rejected rather than overwritten.

Current scope

The current version supports indexed local Markdown plus ordinary image assets. All five image tools use knowledgeBase.root as the boundary; saving defaults to images/. Remote access from ChatGPT is supported through OpenAI Secure MCP Tunnel, which externally launches and bridges the existing local stdio server.

PDF/DOCX/OCR, arbitrary binary upload, image embeddings/search, image resizing/transcoding/thumbnails, automatic Markdown image insertion, a media database, a web UI, Git history indexing, direct remote HTTP MCP transport, application-level authentication, multi-user mode, CRDT, and automatic merge are outside the current scope.

Addressable Markdown list items and atomic quotes

kb_read exposes individual list items as list_item using li1, 1.li2, 1.li2.li1 plus the normal two-hash anchor suffix. Parent Text excludes nested item text. replace_element (and its deprecated alias replace) changes the item's own source content before its first nested child, preserving all descendants and untouched parent-owned content after them; it checks selfHash. Explicit replace_subtree or delete replaces/removes the whole subtree and requires both current hashes. Adjacent insertions check self hash and accept one source-indented sibling with a compatible list marker; neither blank lines nor ordered numbers are silently normalized.

A stand-alone Markdown quote is one opaque block_quote with qN or 1.qN, regardless of internal Markdown (including > - item). It can only be read, searched, deleted, inserted around, or replaced as a whole. Older paragraph pointers suppressed inside lists/quotes must be re-read.

List-item editing uses exact half-open source ranges: the subtree contains every descendant, whereas the parent's own text consists only of its non-child source segments. replace_element replaces the contiguous own prefix, preserving any nested items and later parent-owned source; replace_subtree replaces the full subtree; delete consumes its terminating line ending when present; insert_after inserts after all descendants. No AST serialization or document-wide whitespace normalization is performed, and unmodified source bytes (including BOM and mixed EOL) are retained.

A list insertion or replacement must supply one item with the target's physical indentation and compatible marker style (ordered marker numbers may differ). replace_element accepts an item without child items; it preserves existing children, and fails closed if a safe source prefix cannot be proven. To deliberately change the nested list, use replace_subtree with a current subtreeHash. Nested items are supported in replace_subtree and sibling insertions; code, HTML comments, and other Markdown blocks remain permitted within the new item when Markdig assigns them to it. Independent blocks outside the new subtree are rejected. For example, - Updated\n\n<!-- detached --> cannot replace a top-level item when the comment splits its list; - Updated\n\n <!-- nested --> is allowed if Markdig assigns the comment to the item. A quote fragment similarly must form exactly one opaque quote without absorbing its neighbors.

The server re-parses the entire proposed result before writing: existing elements, non-addressable blocks, list-container ownership, parent relationships, and neighboring boundaries must survive. Ambiguous or overlapping edits (including multiple operations in one kb_patch) fail atomically with a structural error; the source file is left unchanged. Unsupported indentation, list splitting/merging, and quote-boundary ambiguity are intentionally rejected instead of being auto-formatted.

Search pointer and read hint still identify the first chunk element, not necessarily the exact list-item match. Chunker v5 requires explicit derived-index rebuild: kb_reindex(force=true) or CLI --reindex --force. No Markdown migration is performed.

Semantic Markdown tables

Standalone GFM pipe tables appear as atomic table elements with section-local tN pointers. Address kb_read with a table's hashed pointer to obtain table.columns (name, index, alignment) and table.rows (data-row indexes beginning at zero and visible cell text). Ordinary document paging does not duplicate the structured projection.

The new kb_edit_table tool takes path, a current pointer of the form tN~selfHash~subtreeHash, and one of seven actions: update_cells, insert_row, delete_row, insert_column, delete_column, rename_column, or set_alignment. Example:

json
{
  "path": "recipes.md",
  "pointer": "1.t1~<selfHash>~<subtreeHash>",
  "action": "update_cells",
  "updates": [
    {
      "where": { "column": "Product", "equals": "Chicken" },
      "column": "Price",
      "value": "20"
    }
  ]
}

Prefer where when a row value is unique and column when the header is unique; otherwise use numeric rowIndex and columnIndex. Lookups are exact and case-sensitive. update_cells batches multiple updates atomically. Values default to literal text, safely escaping Markdown; valueFormat=markdown permits intentional inline formatting. Newly inserted row values and column defaults are literal text.

Successful responses contain the new table pointer, updated sourceHash and index synchronization status. Reuse the returned pointer to chain edits; on CONFLICT, reread the table. Edits reject tables with extra ignored physical cells, preserve unrelated Markdown, and use the existing atomic workspace writer. Tables inside code, lists or block quotes have no independent table pointers. To remove a table entirely, use kb_patch.

Table parsing changes the derived index format to chunker version 6. Rebuild incompatible indexes via kb_reindex(force=true) or CLI --reindex --force; no Markdown-file migration is needed.

Similar modules

Browse Developer documentation