semantic-code-mcp
Use this MCP server to index codebases and retrieve ranked code chunks through search.
Search code by words, with optional meaning-based search when configured; symbol-like queries prioritize definitions.
Auto-index on first search and incrementally re-index changed files.
Manually index a codebase, optionally forcing a full rebuild.
Check index status: indexed state, last update, file/chunk counts, staleness, and embedded-chunk details.
Filter results by chunk kind, language, path glob, and exclusion glob; limit and debug results.
Supports Python, Rust, and Markdown, returning file paths, line ranges, names, kinds, parents, content, and ranks.
Enables semantic indexing and searching of Python codebases by using tree-sitter to parse AST structures like functions, classes, and methods for precise code retrieval.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@semantic-code-mcpfind the logic for processing user authentication in /home/user/project"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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)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
Storage — the chunks and a full-text index of their words in one SQLite database per index (LanceDB, an optional extra, in its place)
Search — by words (BM25), the chunks named as the query first
Meaning, by a setting — with the extra
semantic-code-mcp[meaning]andSEMANTIC_CODE_MCP_SEARCH_MODE=words_and_meaningthe 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-mcpClaude 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-mcpOn an earlier Claude Code, add the marketplace first:
/plugin marketplace add vrppaul/semantic-code-mcp
/plugin install semantic-code@semantic-code-marketplaceClaude Code, the server alone:
claude mcp add --scope user semantic-code -- uvx semantic-code-mcpThe 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-mcpOr 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 |
|
| required | The words to find, as code would write them, or a name ( |
|
| 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 |
|
|
| Maximum number of results, at most 50 |
|
| all | Only chunks of these kinds: |
|
| all | Only chunks of files in these languages: |
|
| none | Only files this glob names, as in a |
|
| none | No file this glob names, read as |
|
|
| 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 |
|
| 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 |
|
|
| 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 |
|
| 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 |
|
| Where indexes are stored |
|
| What keeps the indexes: |
|
| Store index in |
|
| A sentence-transformers model that pools by the mean, with an ONNX export in one file ( |
|
| Enable debug logging |
|
| Enable pyinstrument profiling (a development checkout only: pyinstrument is a dev dependency) |
|
| Let |
|
| How long git may take to list the files before discovery walks the file system instead |
|
| 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 ( |
|
| How every search ranks: |
|
| 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 |
|
| The same, for a result whose words match the query's; at most |
| caches, virtualenvs, | JSON list of patterns skipped in every mode. A pattern without |
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-mcpTech Stack
Component | Choice | Rationale |
MCP Framework | mcp 2.x ( | 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 runsThe 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.0CI 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:
A tree-sitter grammar package (e.g.
tree-sitter-javascript)A chunker subclass that walks the AST and extracts meaningful chunks
Steps:
uv add tree-sitter-mylangAdd 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 typesRegister 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 presenterssrc/semantic_code_mcp/services/— indexing, search, the registry of projectssrc/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 sourcesrc/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 themeaningextra, and the stores (SQLite, LanceDB) behind one contractsrc/semantic_code_mcp/domain/— models and interfacesevals/— 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'sSpeedworkflow times on three systems), and agents' sessions with and without the search (agents, on corpora it names)docs/decisions/— architecture decision recordsTODO.md— epics and planningCHANGELOG.md— completed work (Keep a Changelog format).claude/rules/— context-specific coding rules for AI agents
License
MIT
Available Tools
3 toolsindex_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.
| Name | Required | Description | Default |
|---|---|---|---|
| force | No | ||
| project_path | No | Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository. |
Output Schema
| Name | Required | Description |
|---|---|---|
| project_root | Yes | The root of the project whose index this is: a directory inside a git repository is indexed, and reported, with its whole repository. |
| files_deleted | Yes | |
| files_indexed | Yes | |
| chunks_indexed | Yes | |
| duration_seconds | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does substantial work: it discloses that no embeddings are computed here, that a meaning-search server embeds chunks lazily on its next search, and that force drops existing vectors for unpinned models. It stops short of cost, duration, or concurrency behavior for large repositories, so it is rich but not complete.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The content is front-loaded and organized with Args/Returns sections, but the embedding behavior is explained twice in near-duplicate sentences, and phrasing like "a model whose weights nothing pins, as a local path" is convoluted enough to slow parsing. It is informative but not tight.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists and the description still summarizes the return value (indexed root, what was indexed and deleted), and it handles the tricky path-resolution edge cases an agent would otherwise get wrong. Only operational scale questions (time, resource use on large repos) are left open.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is only 50% (force has no schema description), and the Args section compensates fully for force and adds real meaning beyond the schema for project_path: a directory inside a git repo indexes the whole repository, with exceptions for ignored paths and the user's home directory. That repository-expansion rule is not inferable from the schema at all.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Index a codebase's words for search") and immediately scopes what is parsed (Python, Rust, Markdown into functions, classes, docstrings, sections) and what is not done ("it embeds nothing"). It also explicitly separates itself from the sibling search_code, so an agent can choose between them without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit when-not: "`search_code` indexes on its own; call this only to re-index everything." The force parameter section then defines the second usage mode (full re-index vs. incremental), so both branches of when-to-call are covered with the named alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| project_path | No | Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository. |
Output Schema
| Name | Required | Description |
|---|---|---|
| model | Yes | |
| manifest | Yes | |
| is_indexed | Yes | |
| being_built | Yes | A run holds the index now, this session's or another's, and may be writing it: until it ends, the counts may be what it has written so far; a search waits for it. |
| files_count | Yes | |
| chunks_count | Yes | |
| last_updated | Yes | |
| project_root | Yes | The root of the project whose index this is: a directory inside a git repository is indexed, and reported, with its whole repository. |
| stale_files_count | Yes | |
| embedded_chunks_count | Yes | How many of the chunks have the server's model's vectors; a server set to search by meaning too embeds the rest before it answers. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full load and does substantial work: it explains that each chunker version keeps its own index slice, that each model's vectors are stored separately, and that an in-flight run (this session's or another's) can make reported counts a partial snapshot. That concurrency caveat is exactly the kind of behavior an agent cannot infer from a schema. It never states outright that the call is a side-effect-free read, which is why it stops short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core purpose and the staleness note are front-loaded, which is good, but the prose is dense and long for a single optional parameter. The Args block largely restates the schema description and the Returns block restates information that the output schema already supplies, so a meaningful share of the text does not earn its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a one-optional-parameter read tool with an output schema and no annotations, the description covers what the call does, the argument's path-resolution edge cases, and concurrent-run semantics. The only real gap is return-value framing, which the output schema already handles, making the verbose Returns paragraph redundant rather than missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the parameter is already documented and the baseline is 3. The description does add nuance beyond the schema text: the git-repository promotion rule, the exception for ignored directories and the user's home directory, and the workspace-root default. That is a modest addition over an already-complete schema, so it lands at the baseline rather than above it.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
"Get the index status for a project" pairs a specific verb with a specific resource, and the enumeration of what the status contains (indexed flag, last update, per-language file/chunk counts, vector coverage, changed files, chunker/model partitioning, active run) removes any ambiguity about what "status" means. It distinguishes itself from search_code via the staleness note, but never mentions the sibling index_codebase, so the read-vs-write split with that tool is left implicit.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Contains an explicit when-not-to-use: staleness should not be checked or acted on manually because search_code re-indexes automatically. That is real routing guidance an agent could otherwise get wrong. What is missing is a positive statement of the scenario that does warrant this call (diagnostics, verifying an index_codebase run finished) rather than a pointer to the indexing sibling.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes | ||
| languages | No | Only chunks of files in these languages. | |
| path_glob | No | 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, and a directory takes what it holds. No character classes (`[ab]`), braces, escapes, a leading `!`, `..` or more than 1000 characters: each is refused with the reason. | |
| chunk_kinds | No | Only chunks of these kinds; Rust's structs, enums, traits and impls are classes. | |
| exclude_glob | No | No file this glob names, read as path_glob is (`tests`, `*_test.py`). | |
| project_path | No | Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository. | |
| include_debug | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| debug | Yes | |
| results | Yes | |
| project_root | Yes | The root of the project whose index answered: as project_path, it searches the whole repository. |
| searched_directory | Yes | The directory the search kept to: project_root, or a directory under it; the search did not look outside it. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses the side effect of automatically indexing the project and re-indexing changed files, the language coverage (Python, Rust, Markdown), that only best matches are returned, and that invalid globs are refused with a reason. It does not cover permissions, cost/latency, or behavior when no results are found, so it stops short of fully transparent.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well front-loaded and conventionally structured with Args/Returns sections, but the Args block duplicates schema text nearly verbatim for several parameters, and phrasing like 'For every place a name is used, Grep: this returns only the best matches' is awkward and costs a read. The examples earn their place; the restated parameter lines mostly do not.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For an eight-parameter tool with no annotations, the definition covers the essentials: what is indexed, when indexing happens, how scoping works, and what a result contains (path, line range, name, kind, owner, content), which the output schema also backs up. Missing only edge-case behavior such as empty result sets or failure modes, which are minor given the output schema's presence.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
At 63% schema coverage, the Args block touches all eight parameters and adds real meaning for the two most important ones: query (write identifiers as the code would, with examples) and project_path (a directory inside the repo narrows the search). The remaining entries (limit, chunk_kinds, languages, include_debug) largely restate the schema descriptions, so the added value is concentrated rather than uniform.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource with unusual precision: 'Find the functions, classes, methods and document sections of a project by the words their code uses.' It also carves out a boundary against plain text search ('For every place a name is used, Grep: this returns only the best matches'), so the agent can tell it apart from a generic grep without opening the schema. The sibling tools index_codebase/index_status are only addressed indirectly via the auto-indexing sentence.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives concrete usage guidance: write the identifiers the code would use, and 'on a miss search again with others', plus three worked query examples. It names the alternative (Grep) and the condition that selects it. There is no explicit statement of when this tool should be avoided in favor of index_codebase or index_status, but the context is otherwise clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.5.0- Changed
index_codebase15 fields changed- added
Input schema / properties / project_path / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - added
Input schema / properties / project_path / defaultAdded value: +null - added
Input schema / properties / project_path / descriptionAdded value: +"Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository." - removed
Input schema / properties / project_path / typeRemoved value: -"string" - removed
Input schema / requiredRemoved value: -[ - "project_path" -] - removed
Output schema / $defsRemoved value: -{ - "ErrorResponse": { - "description": "Error response for tool failures.", - "properties": { - "error": { - "title": "Error", - "type": "string" - } - }, - "required": [ - "error" - ], - "title": "ErrorResponse", - "type": "object" - }, - "IndexCodebaseResponse": { - "description": "Response from index_codebase tool.", - "properties": { - "chunks_indexed": { - "title": "Chunks Indexed", - "type": "integer" - }, - "duration_seconds": { - "title": "Duration Seconds", - "type": "number" - }, - "files_deleted": { - "title": "Files Deleted", - "type": "integer" - }, - "files_indexed": { - "title": "Files Indexed", - "type": "integer" - } - }, - "required": [ - "files_indexed", - "chunks_indexed", - "files_deleted", - "duration_seconds" - ], - "title": "IndexCodebaseResponse", - "type": "object" - } -} - added
Output schema / descriptionAdded value: +"The answer of `index_codebase`." - added
Output schema / properties / chunks_indexedAdded value: +{ + "title": "Chunks Indexed", + "type": "integer" +} - added
Output schema / properties / duration_secondsAdded value: +{ + "title": "Duration Seconds", + "type": "number" +} - added
Output schema / properties / files_deletedAdded value: +{ + "title": "Files Deleted", + "type": "integer" +} - added
Output schema / properties / files_indexedAdded value: +{ + "title": "Files Indexed", + "type": "integer" +} - added
Output schema / properties / project_rootAdded value: +{ + "description": "The root of the project whose index this is: a directory inside a git repository is indexed, and reported, with its whole repository.", + "title": "Project Root", + "type": "string" +} - removed
Output schema / properties / resultRemoved value: -{ - "anyOf": [ - { - "$ref": "#/$defs/IndexCodebaseResponse" - }, - { - "$ref": "#/$defs/ErrorResponse" - } - ], - "title": "Result" -} - changed
Output schema / requiredPrevious value: -[ - "result" -]New value: +[ + "project_root", + "files_indexed", + "chunks_indexed", + "files_deleted", + "duration_seconds" +] - changed
Output schema / titlePrevious value: -"index_codebaseOutput"New value: +"IndexCodebaseResponse"
- Changed
index_status23 fields changed- added
Input schema / properties / project_path / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - added
Input schema / properties / project_path / defaultAdded value: +null - added
Input schema / properties / project_path / descriptionAdded value: +"Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository." - removed
Input schema / properties / project_path / typeRemoved value: -"string" - removed
Input schema / requiredRemoved value: -[ - "project_path" -] - removed
Output schema / $defs / ErrorResponseRemoved value: -{ - "description": "Error response for tool failures.", - "properties": { - "error": { - "title": "Error", - "type": "string" - } - }, - "required": [ - "error" - ], - "title": "ErrorResponse", - "type": "object" -} - added
Output schema / $defs / IndexManifestViewAdded value: +{ + "description": "What built an index: its chunker and the store's schema, each of whose versions keeps\nan index of its own.", + "properties": { + "chunker_version": { + "title": "Chunker Version", + "type": "integer" + }, + "schema_version": { + "title": "Schema Version", + "type": "integer" + } + }, + "required": [ + "chunker_version", + "schema_version" + ], + "title": "IndexManifestView", + "type": "object" +} - removed
Output schema / $defs / IndexStatusResponseRemoved value: -{ - "description": "Response from index_status tool.", - "properties": { - "chunks_count": { - "title": "Chunks Count", - "type": "integer" - }, - "files_count": { - "title": "Files Count", - "type": "integer" - }, - "is_indexed": { - "title": "Is Indexed", - "type": "boolean" - }, - "last_updated": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Last Updated" - } - }, - "required": [ - "is_indexed", - "last_updated", - "files_count", - "chunks_count" - ], - "title": "IndexStatusResponse", - "type": "object" -} - added
Output schema / $defs / ModelViewAdded value: +{ + "description": "The model a search by meaning embeds with, whose vectors `embedded_chunks_count`\ncounts: the index keeps each model's vectors apart.", + "properties": { + "embedder_version": { + "title": "Embedder Version", + "type": "integer" + }, + "model_name": { + "title": "Model Name", + "type": "string" + }, + "model_revision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Revision" + } + }, + "required": [ + "model_name", + "model_revision", + "embedder_version" + ], + "title": "ModelView", + "type": "object" +} - added
Output schema / descriptionAdded value: +"The answer of `index_status`." - added
Output schema / properties / being_builtAdded value: +{ + "description": "A run holds the index now, this session's or another's, and may be writing it: until it ends, the counts may be what it has written so far; a search waits for it.", + "title": "Being Built", + "type": "boolean" +} - added
Output schema / properties / chunks_countAdded value: +{ + "title": "Chunks Count", + "type": "integer" +} - added
Output schema / properties / embedded_chunks_countAdded value: +{ + "description": "How many of the chunks have the server's model's vectors; a server set to search by meaning too embeds the rest before it answers.", + "title": "Embedded Chunks Count", + "type": "integer" +} - added
Output schema / properties / files_countAdded value: +{ + "title": "Files Count", + "type": "integer" +} - added
Output schema / properties / is_indexedAdded value: +{ + "title": "Is Indexed", + "type": "boolean" +} - added
Output schema / properties / last_updatedAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated" +} - added
Output schema / properties / manifestAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/IndexManifestView" + }, + { + "type": "null" + } + ] +} - added
Output schema / properties / modelAdded value: +{ + "$ref": "#/$defs/ModelView" +} - added
Output schema / properties / project_rootAdded value: +{ + "description": "The root of the project whose index this is: a directory inside a git repository is indexed, and reported, with its whole repository.", + "title": "Project Root", + "type": "string" +} - removed
Output schema / properties / resultRemoved value: -{ - "anyOf": [ - { - "$ref": "#/$defs/IndexStatusResponse" - }, - { - "$ref": "#/$defs/ErrorResponse" - } - ], - "title": "Result" -} - added
Output schema / properties / stale_files_countAdded value: +{ + "title": "Stale Files Count", + "type": "integer" +} - changed
Output schema / requiredPrevious value: -[ - "result" -]New value: +[ + "project_root", + "is_indexed", + "last_updated", + "files_count", + "chunks_count", + "embedded_chunks_count", + "model", + "stale_files_count", + "manifest", + "being_built" +] - changed
Output schema / titlePrevious value: -"index_statusOutput"New value: +"IndexStatusResponse"
- Changed
search_code35 fields changed- added
Input schema / $defsAdded value: +{ + "ChunkKind": { + "description": "What a chunk is. Rust's structs, enums, traits and impls are classes.", + "enum": [ + "module", + "function", + "class", + "method", + "section" + ], + "title": "ChunkKind", + "type": "string" + }, + "Language": { + "description": "A language the chunkers read.", + "enum": [ + "python", + "rust", + "markdown" + ], + "title": "Language", + "type": "string" + } +} - added
Input schema / properties / chunk_kindsAdded value: +{ + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ChunkKind" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only chunks of these kinds; Rust's structs, enums, traits and impls are classes.", + "title": "Chunk Kinds" +} - added
Input schema / properties / exclude_globAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "No file this glob names, read as path_glob is (`tests`, `*_test.py`).", + "title": "Exclude Glob" +} - added
Input schema / properties / include_debugAdded value: +{ + "default": false, + "title": "Include Debug", + "type": "boolean" +} - added
Input schema / properties / languagesAdded value: +{ + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Language" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Only chunks of files in these languages.", + "title": "Languages" +} - added
Input schema / properties / limit / maximumAdded value: +50 - added
Input schema / properties / limit / minimumAdded value: +1 - added
Input schema / properties / path_globAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "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, and a directory takes what it holds. No character classes (`[ab]`), braces, escapes, a leading `!`, `..` or more than 1000 characters: each is refused with the reason.", + "title": "Path Glob" +} - added
Input schema / properties / project_path / anyOfAdded value: +[ + { + "type": "string" + }, + { + "type": "null" + } +] - added
Input schema / properties / project_path / defaultAdded value: +null - added
Input schema / properties / project_path / descriptionAdded value: +"Absolute path to the project, or to a directory inside its git repository. Left out, the client's workspace root: the directory Claude Code was started in, which must lie in a git repository." - removed
Input schema / properties / project_path / typeRemoved value: -"string" - changed
Input schema / requiredPrevious value: -[ - "query", - "project_path" -]New value: +[ + "query" +] - added
Output schema / $defs / ChunkKindAdded value: +{ + "description": "What a chunk is. Rust's structs, enums, traits and impls are classes.", + "enum": [ + "module", + "function", + "class", + "method", + "section" + ], + "title": "ChunkKind", + "type": "string" +} - removed
Output schema / $defs / ErrorResponseRemoved value: -{ - "description": "Error response for tool failures.", - "properties": { - "error": { - "title": "Error", - "type": "string" - } - }, - "required": [ - "error" - ], - "title": "ErrorResponse", - "type": "object" -} - removed
Output schema / $defs / FormattedSearchResultRemoved value: -{ - "description": "A search result formatted for response.", - "properties": { - "chunk_type": { - "title": "Chunk Type", - "type": "string" - }, - "content": { - "title": "Content", - "type": "string" - }, - "file_path": { - "title": "File Path", - "type": "string" - }, - "line_end": { - "title": "Line End", - "type": "integer" - }, - "line_start": { - "title": "Line Start", - "type": "integer" - }, - "name": { - "title": "Name", - "type": "string" - }, - "score": { - "title": "Score", - "type": "number" - }, - "truncated": { - "default": false, - "title": "Truncated", - "type": "boolean" - } - }, - "required": [ - "file_path", - "line_start", - "line_end", - "name", - "chunk_type", - "content", - "score" - ], - "title": "FormattedSearchResult", - "type": "object" -} - removed
Output schema / $defs / IndexResultSummaryRemoved value: -{ - "description": "Summary of indexing result for search debug info.", - "properties": { - "chunks_indexed": { - "title": "Chunks Indexed", - "type": "integer" - }, - "files_indexed": { - "title": "Files Indexed", - "type": "integer" - } - }, - "required": [ - "files_indexed", - "chunks_indexed" - ], - "title": "IndexResultSummary", - "type": "object" -} - removed
Output schema / $defs / IndexStatusSummaryRemoved value: -{ - "description": "Summary of index status for search debug info.", - "properties": { - "chunks_count": { - "title": "Chunks Count", - "type": "integer" - }, - "files_count": { - "title": "Files Count", - "type": "integer" - }, - "was_stale": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Was Stale" - } - }, - "required": [ - "files_count", - "chunks_count" - ], - "title": "IndexStatusSummary", - "type": "object" -} - added
Output schema / $defs / IndexingSummaryViewAdded value: +{ + "description": "What the search indexed before it searched.", + "properties": { + "chunks_indexed": { + "title": "Chunks Indexed", + "type": "integer" + }, + "files_indexed": { + "title": "Files Indexed", + "type": "integer" + } + }, + "required": [ + "files_indexed", + "chunks_indexed" + ], + "title": "IndexingSummaryView", + "type": "object" +} - removed
Output schema / $defs / SearchDebugInfoRemoved value: -{ - "description": "Debug information included in search response.", - "properties": { - "index_result": { - "$ref": "#/$defs/IndexResultSummary" - }, - "index_status": { - "$ref": "#/$defs/IndexStatusSummary" - }, - "stats": { - "$ref": "#/$defs/SearchStats" - }, - "timings": { - "$ref": "#/$defs/SearchTimings" - } - }, - "required": [ - "timings", - "stats", - "index_status", - "index_result" - ], - "title": "SearchDebugInfo", - "type": "object" -} - added
Output schema / $defs / SearchDebugViewAdded value: +{ + "description": "Timings and counts of a search, sent only when asked for.", + "properties": { + "indexing": { + "anyOf": [ + { + "$ref": "#/$defs/IndexingSummaryView" + }, + { + "type": "null" + } + ] + }, + "stats": { + "$ref": "#/$defs/SearchStatsView" + }, + "timings": { + "$ref": "#/$defs/SearchTimingsView" + } + }, + "required": [ + "timings", + "stats", + "indexing" + ], + "title": "SearchDebugView", + "type": "object" +} - removed
Output schema / $defs / SearchResponseRemoved value: -{ - "description": "Complete search response with results and debug info.", - "properties": { - "debug": { - "$ref": "#/$defs/SearchDebugInfo" - }, - "results": { - "items": { - "$ref": "#/$defs/FormattedSearchResult" - }, - "title": "Results", - "type": "array" - } - }, - "required": [ - "results", - "debug" - ], - "title": "SearchResponse", - "type": "object" -} - added
Output schema / $defs / SearchResultViewAdded value: +{ + "description": "One hit as the agent sees it; `content` is cut to a line limit, `truncated` says so.", + "properties": { + "content": { + "title": "Content", + "type": "string" + }, + "file_path": { + "title": "File Path", + "type": "string" + }, + "kind": { + "$ref": "#/$defs/ChunkKind" + }, + "line_end": { + "title": "Line End", + "type": "integer" + }, + "line_start": { + "title": "Line Start", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Parent" + }, + "truncated": { + "title": "Truncated", + "type": "boolean" + } + }, + "required": [ + "file_path", + "line_start", + "line_end", + "name", + "kind", + "parent", + "content", + "truncated" + ], + "title": "SearchResultView", + "type": "object" +} - removed
Output schema / $defs / SearchStatsRemoved value: -{ - "description": "Statistics about search results.", - "properties": { - "filtered_out": { - "title": "Filtered Out", - "type": "integer" - }, - "results_count": { - "title": "Results Count", - "type": "integer" - }, - "tokens_estimate": { - "title": "Tokens Estimate", - "type": "integer" - }, - "unique_files": { - "title": "Unique Files", - "type": "integer" - } - }, - "required": [ - "results_count", - "filtered_out", - "unique_files", - "tokens_estimate" - ], - "title": "SearchStats", - "type": "object" -} - added
Output schema / $defs / SearchStatsViewAdded value: +{ + "description": "What a search kept and dropped.", + "properties": { + "filtered_out": { + "title": "Filtered Out", + "type": "integer" + }, + "results_count": { + "title": "Results Count", + "type": "integer" + }, + "tokens_estimate": { + "title": "Tokens Estimate", + "type": "integer" + }, + "unique_files": { + "title": "Unique Files", + "type": "integer" + } + }, + "required": [ + "results_count", + "filtered_out", + "unique_files", + "tokens_estimate" + ], + "title": "SearchStatsView", + "type": "object" +} - removed
Output schema / $defs / SearchTimingsRemoved value: -{ - "description": "Timing breakdown for search operation.", - "properties": { - "embedding_ms": { - "title": "Embedding Ms", - "type": "number" - }, - "indexing_ms": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Indexing Ms" - }, - "search_ms": { - "title": "Search Ms", - "type": "number" - }, - "total_ms": { - "title": "Total Ms", - "type": "number" - } - }, - "required": [ - "embedding_ms", - "search_ms", - "total_ms" - ], - "title": "SearchTimings", - "type": "object" -} - added
Output schema / $defs / SearchTimingsViewAdded value: +{ + "description": "Where the time of a search went, in milliseconds.", + "properties": { + "embedding_ms": { + "title": "Embedding Ms", + "type": "number" + }, + "indexing_ms": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Indexing Ms" + }, + "search_ms": { + "title": "Search Ms", + "type": "number" + }, + "total_ms": { + "title": "Total Ms", + "type": "number" + } + }, + "required": [ + "embedding_ms", + "search_ms", + "total_ms", + "indexing_ms" + ], + "title": "SearchTimingsView", + "type": "object" +} - added
Output schema / descriptionAdded value: +"The answer of `search_code`." - added
Output schema / properties / debugAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/SearchDebugView" + }, + { + "type": "null" + } + ] +} - added
Output schema / properties / project_rootAdded value: +{ + "description": "The root of the project whose index answered: as project_path, it searches the whole repository.", + "title": "Project Root", + "type": "string" +} - removed
Output schema / properties / resultRemoved value: -{ - "anyOf": [ - { - "$ref": "#/$defs/SearchResponse" - }, - { - "$ref": "#/$defs/ErrorResponse" - } - ], - "title": "Result" -} - added
Output schema / properties / resultsAdded value: +{ + "items": { + "$ref": "#/$defs/SearchResultView" + }, + "title": "Results", + "type": "array" +} - added
Output schema / properties / searched_directoryAdded value: +{ + "description": "The directory the search kept to: project_root, or a directory under it; the search did not look outside it.", + "title": "Searched Directory", + "type": "string" +} - changed
Output schema / requiredPrevious value: -[ - "result" -]New value: +[ + "project_root", + "searched_directory", + "results", + "debug" +] - changed
Output schema / titlePrevious value: -"search_codeOutput"New value: +"SearchCodeResponse"
3 tool updates
v0.1.0- First observed
index_codebase - First observed
index_status - First observed
search_code
TDQS
Scored across 3 tools
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.
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.
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.
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
Related MCP Connectors
An MCP server for search and retrieval over a local LanceDB database
An MCP server that gives your AI access to the source code and docs of all public github repos
Capability registry for the agentic economy. Semantic search over verified MCP server listings.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceA 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-
- AlicenseAqualityDmaintenanceMCP server for semantic code search that indexes your codebase and allows AI editors to search using natural language queries.921 npm52MIT
- FlicenseNot gradedqualityDmaintenanceA 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.-
- FlicenseBqualityDmaintenanceA local MCP server that indexes codebases using Tree-sitter AST parsing and LanceDB, enabling AI models to search across repositories with sub-second latency.5-