Skip to main content
Glama
semantic-code-mcp
by vrppaul

semantic-code-mcp

MCP server that provides code search for Claude Code. Instead of iterative grep/glob, it indexes your codebase's words in seconds and returns ranked functions, classes and sections, a definition first when you ask its name. A setting adds a search by meaning, which embeds the code; agents measured with and without it found no gain from it.

Supports Python, Rust, and Markdown — more languages planned.

How It Works

Claude Code ──(MCP/STDIO)──▶ semantic-code-mcp server
                                    │
                    ┌───────────────┼───────────────┐
                    ▼               ▼               ▼
              AST Chunker      Embedder         SQLite
             (tree-sitter)   (onnxruntime)  (words, vectors)
  1. Chunking — tree-sitter parses source files into functions, classes, methods, structs, traits, markdown sections, etc.; a repository of 8 MiB of source or more is parsed in up to four processes beside the server, half its cores, for the seconds of its first index

  2. Storage — the chunks and a full-text index of their words in one SQLite database per index (LanceDB, an optional extra, in its place)

  3. Search — by words (BM25), the chunks named as the query first

  4. Meaning, by a setting — with the extra semantic-code-mcp[meaning] and SEMANTIC_CODE_MCP_SEARCH_MODE=words_and_meaning the server embeds each chunk once with onnxruntime and the model's ONNX export (all-MiniLM-L6-v2, 384d, no torch), keeps each model's vectors beside the chunks, and fuses every search by meaning with one by words by rank

Indexing is incremental: the index records each file's modification time and size, and re-indexes the files that differ. Between searches the server follows each repository's files with the system's own watcher (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows), so a search reads only what changed; it scans every file where no watcher can tell, and every 5 minutes as a check. Each chunker version and storage layout keeps an index of its own, so a new version builds a new index once, and switching back finds the old one; each model's vectors are kept apart inside it, so another model embeds again without chunking again; an index no session used for a week is removed. In a git repository, git lists the files to index — tracked ones and untracked ones .gitignore does not exclude; elsewhere the server walks the file system. The embedding model loads on the first search of a server set to search by meaning, never for a search by words.

Related MCP server: codebaxing

Installation

One command on every platform, about 95 MB installed. A search by meaning needs the model's libraries, the extra semantic-code-mcp[meaning] (190 MB in all: uvx --from 'semantic-code-mcp[meaning]' semantic-code-mcp), and SEMANTIC_CODE_MCP_SEARCH_MODE=words_and_meaning; such a server fetches the model's ONNX file (90 MB) from the Hugging Face hub once, on its first search:

uvx semantic-code-mcp

Claude Code, as a plugin — the server and the skill that tells the agent how to word a search and when to Grep instead, in one install (Claude Code 2.1.275 or later):

/plugin install semantic-code --marketplace vrppaul/semantic-code-mcp

On an earlier Claude Code, add the marketplace first:

/plugin marketplace add vrppaul/semantic-code-mcp
/plugin install semantic-code@semantic-code-marketplace

Claude Code, the server alone:

claude mcp add --scope user semantic-code -- uvx semantic-code-mcp

The skill alone, for an agent that has the server some other way:

npx skills add vrppaul/semantic-code-mcp
{
  "mcpServers": {
    "semantic-code": {
      "command": "uvx",
      "args": ["semantic-code-mcp"]
    }
  }
}

Updating

uvx caches the installed version. To get the latest release:

uvx --upgrade semantic-code-mcp

Or pin a specific version in your MCP config:

claude mcp add --scope user semantic-code -- uvx semantic-code-mcp@<version>

MCP Tools

search_code

Search code by the words it uses. Auto-indexes on first search.

Parameter

Type

Default

Description

query

str

required

The words to find, as code would write them, or a name (Stack.push), whose chunks come first

project_path

str

the client's workspace root, inside a git repository

Absolute path to the project, or to a directory inside its git repository: the repository is indexed once, and a search keeps to the directory; not a repository that ignores the directory or holds the user's home directory, where the directory is a project of its own

limit

int

10

Maximum number of results, at most 50

chunk_kinds

list[str]

all

Only chunks of these kinds: module, function, method, class, section

languages

list[str]

all

Only chunks of files in these languages: python, rust, markdown

path_glob

str

none

Only files this glob names, as in a .gitignore: without a slash a name at any depth below the directory project_path names (*.rs, and tests/, as a trailing slash does not count), with one before a name a path from that directory (src/**/*.py, /tests); * stays within a directory, ** crosses any number, even inside a name (src**.py, where a .gitignore reads it as *), ? is one character, a directory takes what it holds; a character class ([ab]), braces, escapes, a leading !, .. or more than 1000 characters is refused with the reason

exclude_glob

str

none

No file this glob names, read as path_glob is

include_debug

bool

False

Also return timings and counts of the search

Returns project_root (the project's root: its repository's, or the directory itself outside git; given as project_path, it searches all of it), searched_directory (the directory the search kept to: project_root, or a directory under it) and ranked results with file_path (absolute), line_start, line_end, name, kind (module, function, method, class, section; Rust's structs, enums, traits and impls are classes), parent (the class, impl, module or heading the chunk sits in, as Stack for Stack.push), content (cut to 50 lines, truncated says so); results come best first. A failure — a missing or relative path, an index that cannot be written — comes back as a tool error with its cause.

index_codebase

Index a codebase's words for search; it embeds nothing, and a server set to search by meaning embeds the chunks on its next search. Only processes new and changed files unless force=True.

Parameter

Type

Default

Description

project_path

str

the client's workspace root, inside a git repository

Absolute path to the project, or to a directory inside its git repository: the repository is indexed once, and a search keeps to the directory; not a repository that ignores the directory or holds the user's home directory, where the directory is a project of its own

force

bool

False

Re-index all files regardless of changes

Returns project_root (a directory inside a repository indexes the repository from its root), files_indexed, chunks_indexed, files_deleted and duration_seconds.

index_status

Check indexing status for a project.

Parameter

Type

Default

Description

project_path

str

the client's workspace root, inside a git repository

Absolute path to the project, or to a directory inside its git repository: the repository is indexed once, and a search keeps to the directory; not a repository that ignores the directory or holds the user's home directory, where the directory is a project of its own

Returns project_root (a directory inside a repository reports the repository's root), is_indexed, last_updated, files_count, chunks_count, embedded_chunks_count (the chunks with the server's model's vectors, which a server set to search by meaning reads without embedding first), model (the model, its revision and the embedder version whose vectors embedded_chunks_count counts), stale_files_count, manifest (the chunker and storage versions that built the index) and being_built (a run, this session's or another's, holds the index now: until it ends, the counts may be what it has written so far).

Configuration

All settings are environment variables with the SEMANTIC_CODE_MCP_ prefix (via pydantic-settings):

Variable

Default

Description

SEMANTIC_CODE_MCP_CACHE_DIR

~/.cache/semantic-code-mcp

Where indexes are stored

SEMANTIC_CODE_MCP_INDEX_STORE

sqlite

What keeps the indexes: sqlite, or lancedb, whose libraries come with the extra semantic-code-mcp[lancedb] (uvx --from 'semantic-code-mcp[lancedb]' semantic-code-mcp). Each keeps indexes of its own, so a change builds the index again once

SEMANTIC_CODE_MCP_LOCAL_INDEX

false

Store index in .semantic-code/ at the root of each project's repository; it holds a .gitignore, so git never shows it

SEMANTIC_CODE_MCP_EMBEDDING_MODEL

all-MiniLM-L6-v2

A sentence-transformers model that pools by the mean, with an ONNX export in one file (onnx/model.onnx), by hub name or directory. Only the default model's vectors are tested against sentence-transformers'; another is read from its configs as well as the embedder can, and refused where it can see the vectors would differ

SEMANTIC_CODE_MCP_DEBUG

false

Enable debug logging

SEMANTIC_CODE_MCP_PROFILE

false

Enable pyinstrument profiling (a development checkout only: pyinstrument is a dev dependency)

SEMANTIC_CODE_MCP_USE_GITIGNORE

true

Let .gitignore decide what is indexed. Set false for a repo whose .gitignore hides source on purpose (an allowlist such as /*); the server then walks the file system and applies the ignore patterns only

SEMANTIC_CODE_MCP_GIT_TIMEOUT_SECONDS

10

How long git may take to list the files before discovery walks the file system instead

SEMANTIC_CODE_MCP_WATCH_FILES

true

Follow each repository's files with the system's own watcher, so a search reads what changed since the last one instead of every file, with a full scan every 5 minutes as a check: inotify on Linux, which takes a watch for each directory git does not ignore, from the limit your editor shares (fs.inotify.max_user_watches), and an inotify instance per project; FSEvents on macOS, a stream for the project's root and one for each of git's directories outside it; ReadDirectoryChangesW on Windows, a handle of the project's root, which keeps a directory holding it from being moved meanwhile. Past those limits, outside a repository, with submodules, and on a network or FUSE file system (on macOS and Windows, the one the project's root lies on) each search scans the files, as with false

SEMANTIC_CODE_MCP_SEARCH_MODE

words

How every search ranks: words, BM25 over the chunks' words, no model loaded; or words_and_meaning, the model's vectors searched too and fused with the words by rank. The first search of a project by meaning embeds every chunk inside the call, minutes at 20k chunks and about an hour at 90k; later ones embed what changed. Meaning needs the extra semantic-code-mcp[meaning]: without it a server set to it stops as it starts, naming the extra

SEMANTIC_CODE_MCP_MIN_SIMILARITY

0.35

In a search by meaning, how close in meaning to the query, a cosine similarity, a result no keyword matched must be to come back; set for the default model, whose scale another model does not share. Below 0.25, lower MIN_SIMILARITY_WHEN_WORDS_MATCH with it, or the server refuses to start

SEMANTIC_CODE_MCP_MIN_SIMILARITY_WHEN_WORDS_MATCH

0.25

The same, for a result whose words match the query's; at most MIN_SIMILARITY. A result named as the query comes back however far

SEMANTIC_CODE_MCP_IGNORE_PATTERNS

caches, virtualenvs, node_modules, top-level build/, dist/, target/

JSON list of patterns skipped in every mode. A pattern without / matches a name at any depth (node_modules); one with / matches from the project root (docs/**)

Every session on one project should run with the same IGNORE_PATTERNS and USE_GITIGNORE: an index keeps the files its last run found, so two sessions that find different files delete each other's at every search.

Pass environment variables via the env field in your MCP config:

{
  "mcpServers": {
    "semantic-code": {
      "command": "uvx",
      "args": ["semantic-code-mcp"],
      "env": {
        "SEMANTIC_CODE_MCP_DEBUG": "true",
        "SEMANTIC_CODE_MCP_LOCAL_INDEX": "true"
      }
    }
  }
}

Or with Claude Code CLI:

claude mcp add --scope user semantic-code \
  -e SEMANTIC_CODE_MCP_DEBUG=true \
  -e SEMANTIC_CODE_MCP_LOCAL_INDEX=true \
  -- uvx semantic-code-mcp

Tech Stack

Component

Choice

Rationale

MCP Framework

mcp 2.x (MCPServer)

The official Python SDK, STDIO transport

Embeddings

onnxruntime over the model's ONNX export

Local, no API costs, no torch

Index

SQLite with FTS5

In Python's standard library: one file, a write in one transaction, no server; LanceDB as an optional extra

Chunking

tree-sitter

AST-based, respects code structure

Development

uv sync                            # Install dependencies
uv run pre-commit install --hook-type pre-commit --hook-type commit-msg
uv run python -m semantic_code_mcp # Run server
uv run pytest                      # Run tests
uv run pre-commit run --all-files  # Every gate a commit runs

The gates are one list, .pre-commit-config.yaml: ruff (lint and format), ty (types), import-linter (the package's layers), vulture (code nothing calls), the project's code rules (nested functions, short names, models taken apart by field names), bandit (security), deptry (dependencies as declared), typos, a check that every test module loads, a check of the agent instruction files, the skill's gate (the skill and the server's instructions name what the server has), validate-pyproject, and the file checks of pre-commit-hooks; commit messages are held to Conventional Commits. CI runs the same hooks and the tests on every push to master and every pull request. See CONTRIBUTING.md.

Releasing

Versions are derived from git tags automatically (hatch-vcs) — there's no hardcoded version in pyproject.toml.

git tag v0.2.0
git push origin v0.2.0

CI runs the whole suite on the tagged commit, then builds the package, publishes to PyPI, and creates a GitHub Release with auto-generated notes. A required reviewer on the pypi environment would add the owner's approval before publishing; until the repository's settings name one, a green CI publishes at once.

Adding a New Language

The chunker system is designed to make adding languages straightforward. Each language needs:

  1. A tree-sitter grammar package (e.g. tree-sitter-javascript)

  2. A chunker subclass that walks the AST and extracts meaningful chunks

Steps:

uv add tree-sitter-mylang

Add the language to Language in src/semantic_code_mcp/domain/models.py, and the separator its qualified names are written with (Stack.push, outer::Stack) to PARENT_SEPARATORS beside it; then create src/semantic_code_mcp/chunkers/mylang.py:

from enum import StrEnum, auto
from typing import override

import tree_sitter
import tree_sitter_mylang as tsmylang
from tree_sitter import Node

from semantic_code_mcp.chunkers.base import BaseTreeSitterChunker, ChunkCollector, field_text
from semantic_code_mcp.domain.models import ChunkKind, Language


class NodeType(StrEnum):
    function_definition = auto()
    # ... other node types


class FieldName(StrEnum):
    name = auto()


class MyLangChunker(BaseTreeSitterChunker):
    language = Language.mylang
    grammar = tree_sitter.Language(tsmylang.language())
    extensions = (".ml",)

    @override
    def _extract_chunks(self, root: Node, collector: ChunkCollector) -> None:
        for node in root.children:
            match node.type:
                case NodeType.function_definition:
                    name = field_text(node, FieldName.name)
                    if name is not None:
                        collector.add(node, node, ChunkKind.function, name, parent=None)
                # ... other node types

Register it in src/semantic_code_mcp/app.py, and bump CHUNKER_VERSION in src/semantic_code_mcp/chunkers/composite.py, so indexes built before it are built again:

from semantic_code_mcp.chunkers.mylang import MyLangChunker

def build_chunker() -> CompositeChunker:
    return CompositeChunker([PythonChunker(), RustChunker(), MarkdownChunker(), MyLangChunker()])

Project Structure

  • src/semantic_code_mcp/server/ — the MCP tools, their answers and presenters

  • src/semantic_code_mcp/services/ — indexing, search, the registry of projects

  • src/semantic_code_mcp/discovery.py — which files get indexed (git or a file-system walk, ignore patterns)

  • src/semantic_code_mcp/watching/ — each system's watcher of a project's files (inotify, FSEvents, ReadDirectoryChangesW)

  • src/semantic_code_mcp/pools.py — the pool of processes that chunks a first index of much source

  • src/semantic_code_mcp/chunkers/ — language chunkers (base.py, composite.py, python.py, rust.py, markdown.py)

  • src/semantic_code_mcp/embedding/, src/semantic_code_mcp/storage/ — the embedder and the model it loads from the meaning extra, and the stores (SQLite, LanceDB) behind one contract

  • src/semantic_code_mcp/domain/ — models and interfaces

  • evals/ — search quality and speed on pinned corpora (uv run python -m evals quality, speed), speed also on a repository of Sentry's shape made where it runs (evals/generated/, which CI's Speed workflow times on three systems), and agents' sessions with and without the search (agents, on corpora it names)

  • docs/decisions/ — architecture decision records

  • TODO.md — epics and planning

  • CHANGELOG.md — completed work (Keep a Changelog format)

  • .claude/rules/ — context-specific coding rules for AI agents

License

MIT

Available Tools

3 tools
index_codebaseA

Index a codebase's words for search.

Splits Python, Rust and Markdown files into functions, classes, methods, module docstrings and document sections, and stores them with their words; it embeds nothing, and a server set to search by meaning too embeds them on its next search. search_code indexes on its own; call this only to re-index everything.

With force: true, it re-indexes everything even if files haven't changed; a model whose weights nothing pins, as a local path, has its vectors dropped too, so a server set to search by meaning embeds every chunk again on its next search. Otherwise, only new and modified files are indexed (incremental).

Args: project_path: Absolute path to the project; a directory inside a git repository indexes the whole repository, unless the repository ignores it or holds the user's home directory. Left out, the client's workspace root, which must lie in a git repository. force: Re-index all files regardless of changes.

Returns: The root of the project indexed, and what the run indexed and deleted.

Inputs: force, project_path. Returns: The answer of `index_codebase`.

index_statusA

Get the index status for a project.

Returns whether the project is indexed, when it was last updated, how many of its Python, Rust and Markdown files and their chunks are indexed, how many chunks have the model's vectors, how many files changed since, what built the index (each chunker version keeps an index of its own, and each model's vectors are kept apart in it), and whether a run, this session's or another's, holds it now, so its counts may be what the run has written so far.

Note: search_code automatically re-indexes stale files before searching, so there is no need to check or act on staleness manually.

Args: project_path: Absolute path to the project; a directory inside a git repository reports the whole repository's index, unless the repository ignores it or holds the user's home directory. Left out, the client's workspace root, which must lie in a git repository.

Returns: The root of the project whose index this is, and its status, including the counts of files and chunks.

Inputs: project_path. Returns: The answer of `index_status`.

search_codeA

Find the functions, classes, methods and document sections of a project by the words their code uses.

It matches the words of code: write the identifiers and terms the code would use, and on a miss search again with others. A query that is a name, or a qualified one, brings the chunks of that name first:

  • "retry backoff delay attempts"

  • "parse_args config options"

  • "Stack.push"

For every place a name is used, Grep: this returns only the best matches.

Indexes Python, Rust and Markdown files. Automatically indexes the project if not already indexed, and re-indexes any files that have changed since the last search.

Args: query: The words to find, as code would write them, or a name. project_path: Absolute path to the project, or to a directory inside its git repository, which keeps the search to that directory; left out, the client's workspace root, which must lie in a git repository. limit: Maximum number of results to return. chunk_kinds: Only chunks of these kinds. languages: Only chunks of files in these languages. path_glob: Only files this glob names, read from the directory searched. exclude_glob: No file this glob names, read as path_glob is. include_debug: Also return timings and counts of the search.

Returns: The root of the project whose index answered and the directory the search kept to; then matching chunks, best first, with the file's absolute path, line range, name, kind (module, function, method, class, section), the class, impl, module or heading it belongs to, and content.

Inputs: query (required), limit, languages, path_glob, +4 more. Returns: The answer of `search_code`.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 3 tools

    Disambiguation4/5

    The three tools map to distinct operations: retrieval (search_code), bulk indexing (index_codebase), and inspection (index_status). The only overlap is that search_code auto-indexes and re-indexes changed files, but the description explicitly says to call index_codebase 'only to re-index everything', which resolves the boundary.

    Naming Consistency4/5

    All names are snake_case verb_noun (search_code, index_codebase, index_status), which is predictable. Minor deviation: index_status is a read-only getter sharing the 'index_' prefix with the mutating index_codebase, but the meaning is still clear.

    Tool Count4/5

    Three tools is lean but each earns its place for a focused code-search server: search, index, and status. Nothing looks padded or redundant, though the surface is on the thin side of the 3-15 sweet spot.

    Completeness4/5

    The search lifecycle (index, re-index, status, query with filters) is covered with no dead ends for its stated purpose. Gaps are minor: there is no explicit index deletion/reset tool, so force re-index is the only workaround, and no way to fetch chunks by file path without a query.

    Maintenance

    ActivityMaintained
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      C
      maintenance
      A semantic code search MCP server that enables natural language queries against your codebase, supporting features like related file discovery and context expansion, all running locally.
      2
      -
    • A
      license
      A
      quality
      D
      maintenance
      MCP server for semantic code search that indexes your codebase and allows AI editors to search using natural language queries.
      9
      9 npm
      52
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      A local MCP server that enables LLM clients like Claude to perform semantic code search and answer questions about a codebase using tree-sitter parsing and sqlite-vec vector storage.
      -