Semantic Search MCP
by bborbe
README.md
# Semantic Search
[](https://github.com/bborbe/semantic-search/actions/workflows/ci.yml)
[](https://deepwiki.com/bborbe/semantic-search)
Semantic search over markdown files. Find related notes by meaning, not just keywords. Detect duplicates before creating new notes.
Supports two server transports:
- **stdio MCP** — For Claude Code integration (one process per session)
- **HTTP** — Combined MCP-over-HTTP + REST on one port; one warm process shared by all clients
## Features
- Semantic search using sentence-transformers
- Duplicate/similar note detection
- Auto-updating index with file watcher
- Multi-directory support
- Inline tag extraction (`#tag-name`)
## Install
**CPU-only install** — recommended for **macOS** (any Mac, Apple Silicon or Intel) and **Linux/Windows without an NVIDIA GPU**. Saves ~5GB of CUDA binaries. On macOS, Apple GPU (MPS) is still auto-detected and used via PyTorch's built-in MPS backend — the "CPU" label refers only to the absence of CUDA, not to the compute device at runtime.
```bash
uv tool install --index https://download.pytorch.org/whl/cpu \
git+https://github.com/bborbe/semantic-search
```
**CUDA install** — only for **Linux/Windows with a dedicated NVIDIA GPU**. Not applicable to macOS (NVIDIA CUDA is not supported on Mac).
```bash
uv tool install git+https://github.com/bborbe/semantic-search
```
## Upgrade
```bash
uv tool upgrade semantic-search
```
## Server Modes
### stdio MCP (per-session Claude Code)
Spawns one process per Claude Code session. Simple, but each session loads its own ~400 MB–1 GB model copy.
```bash
claude mcp add -s project semantic-search \
--env CONTENT_PATH=/path/to/vault \
-- \
uvx --from git+https://github.com/bborbe/semantic-search semantic-search-mcp serve
```
**Tools available:**
- `search_related(query, top_k=5)` — Find semantically related notes
- `get_content(path, snippet, query, context_lines)` — Retrieve file content from indexed vaults
- `check_duplicates(file_path)` — Detect duplicate/similar notes
### HTTP (shared across all clients)
Single long-running process serves MCP-over-HTTP at `/mcp` plus REST at `/search`, `/duplicates`, `/content`, `/health`, `/reindex`. All Claude Code sessions and REST clients share **one warm indexer**, and each request names the **scope** it wants — so one process gives every client its own view without a second index.
```bash
SEMANTIC_SCOPE_MAP=scopes.yaml semantic-search-http --host 127.0.0.1 --port 8321
```
The variable may be omitted, in which case the scope map is read from `~/.config/semantic-search/config.yaml`.
Scopes are declared in `scopes.yaml` — see [design/per-vault-scoping.md](docs/design/per-vault-scoping.md) for the contract and the fail-closed default. Point Claude Code at it via MCP config; clients differ only by the scope in their URL:
```json
{
"mcpServers": {
"semantic-search": {
"type": "http",
"url": "http://127.0.0.1:8321/mcp?scope=personal"
}
}
}
```
**REST endpoints:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/mcp` | POST | MCP-over-HTTP (Claude Code); scope comes from the URL |
| `/search?q=...&top_k=5&scope=...` | GET | Semantic search within a scope |
| `/duplicates?file=...&threshold=0.85&scope=...` | GET | Find duplicate notes within a scope |
| `/content?path=...&snippet=...&query=...&context_lines=...&scope=...` | GET | Retrieve file content within a scope |
| `/health` | GET | Health check with index stats (scopeless) |
| `/reindex` | GET/POST | Force index rebuild (scopeless) |
`scope` is **required** on `/search`, `/duplicates`, and `/content`. Omitting it returns HTTP 400 `MISSING_SCOPE`; an unknown name returns 400 `UNKNOWN_SCOPE`. Repeating the parameter resolves to the **union** of the named scopes' roots — `/search?q=...&scope=personal&scope=work` searches both scopes in one request, and the order the scopes are named in does not change the result set. There is deliberately no union-wide fallback: naming no scope, or any undeclared name, fails the whole request closed.
**Example queries:**
```bash
# Search
curl 'http://127.0.0.1:8321/search?q=kubernetes+deployment'
# Find duplicates
curl 'http://127.0.0.1:8321/duplicates?file=notes/my-note.md'
# Health check
curl 'http://127.0.0.1:8321/health'
```
### Two-Step Flow
Search for related notes, then fetch the full content of any result:
```bash
# Step 1: Search for related notes
curl 'http://127.0.0.1:8321/search?q=kubernetes+deployment'
# Returns: [{"path": "notes/k8s-guide.md", "score": 0.92}, ...]
# Step 2: Fetch the content of a result
curl 'http://127.0.0.1:8321/content?path=notes/k8s-guide.md'
# Returns: {"path": "/full/resolved/path.md", "content": "# Kubernetes Guide\n...", "mode": "full"}
```
### Snippet Mode
Retrieve a focused snippet around a specific query term within a file:
```bash
# Get a focused snippet around "service mesh" in the file
curl 'http://127.0.0.1:8321/content?path=notes/k8s-guide.md&snippet=true&query=service+mesh&context_lines=10'
# Returns: {"path": "...", "content": "...\n## Service Mesh\n...", "mode": "snippet"}
```
### Remote Deployment
`get_content` and `GET /content` enable remote deployment of semantic-search clients. Callers no longer need filesystem access to the vault directory — all content retrieval happens over HTTP/MCP from any network location. The server enforces path validation: files outside indexed roots are never served.
## Claude Code Plugin
This repo also ships as a Claude Code marketplace plugin with commands for setup, search, and research.
### Install
```bash
claude plugin marketplace add bborbe/semantic-search
claude plugin install semantic-search
```
### Update
```bash
claude plugin marketplace update semantic-search
claude plugin update semantic-search@semantic-search
```
### Quick Start
```bash
# One-shot interactive setup: installs the binary, writes the launchd/systemd
# unit, registers the MCP server in your Claude config.
/semantic-search:configure
# Search indexed markdown
/semantic-search:search kubernetes deployment
# Multi-step research across results
/semantic-search:research kafka backup strategy
```
### Commands
| Command | Description |
|---------|-------------|
| `/semantic-search:configure` | Install `semantic-search-http` as a launchd (macOS) or systemd-user (Linux) service and register the MCP server in Claude Code |
| `/semantic-search:search <query> [top_k]` | Semantic search via the running MCP server |
| `/semantic-search:research <topic>` | Multi-step research — search, categorize, read top sources, synthesize |
## Run in Background
For production-style usage, run `semantic-search-http` as a background service so every Claude Code session (and any REST client) shares one warm process.
| Platform | Guide |
|----------|-------|
| macOS (launchd) | [`docs/launchd-service.md`](docs/launchd-service.md) |
| Linux (systemd) | [`docs/systemd-user-service.md`](docs/systemd-user-service.md) |
Quick example (macOS):
```bash
launchctl load ~/Library/LaunchAgents/com.github.bborbe.semantic-search-http.plist
```
Quick example (Linux):
```bash
systemctl --user enable --now semantic-search-http.service
```
## CLI Commands
One-shot commands without running a server:
```bash
# Search
CONTENT_PATH=/path/to/vault semantic-search search "kubernetes deployment"
# Find duplicates
CONTENT_PATH=/path/to/vault semantic-search duplicates path/to/note.md
```
## Binaries
| Binary | Purpose |
|--------|---------|
| `semantic-search-http` | Combined HTTP server — MCP at `/mcp` + REST endpoints. Run once, share across clients. |
| `semantic-search-mcp` | stdio MCP server — one per Claude Code session. Use when HTTP service is not set up. |
| `semantic-search` | CLI only — `search` and `duplicates` one-shot commands. |
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `CONTENT_PATH` | Directory to index (comma-separated for multiple) | `./content` |
| `LOG_LEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | `INFO` |
### Multiple Directories
Index multiple directories by separating paths with commas:
```bash
CONTENT_PATH=/path/to/vault1,/path/to/vault2,/path/to/docs
```
All directories are indexed together and searched as one unified index.
### Excluding Files with `.semanticignore`
Place a `.semanticignore` file at the root of any vault to exclude paths from indexing. Each vault uses its own rules independently; a missing `.semanticignore` means "index everything" (no change for existing vaults).
**Syntax** — gitignore-style patterns powered by the `pathspec` library's `gitwildmatch` dialect. The full gitignore rule set applies:
- Patterns are matched relative to the vault root.
- Trailing `/` matches directories (and everything inside them): `archive/`
- `**` matches across directory boundaries: `**/draft.md`
- Leading `/` anchors a pattern to the vault root: `/scratch.md`
- `!` negates a pattern (re-includes a previously excluded path): `!archive/keep.md`
**Behaviour** — matching paths are excluded from:
- Full index rebuilds (`rebuild_index`)
- Single-file add/update events (`add_file_to_index`)
- File-watcher `created`/`modified`/`moved` events
The `.semanticignore` file itself is never indexed.
**Runtime reload** — editing `.semanticignore` while the watcher is running triggers an atomic reload of that vault's rules. Subsequent file events immediately use the new patterns; no restart is required.
**Example `.semanticignore`:**
```gitignore
# Exclude an entire directory
archive/
# Exclude all draft files in any subdirectory
**/draft.md
# Re-include one specific file from the excluded directory
!archive/keep.md
# Anchor a pattern to the vault root only
/scratch.md
```
## How It Works
First run downloads a small embedding model (~90MB) and indexes your markdown files (<1s for typical vaults). The index auto-updates when files change via filesystem watcher.
### Indexed Content
Each markdown file is indexed with weighted components:
| Component | Weight | Notes |
|-----------|--------|-------|
| Filename | 3x | |
| Frontmatter `title` | 3x | |
| Frontmatter `tags` | 2x | Merged with inline tags |
| Frontmatter `aliases` | 2x | |
| Inline tags (`#tag`) | 2x | Extracted from body |
| First H1 heading | 2x | |
| Body content | 1x | First 500 words |
## Development
```bash
# Clone
git clone https://github.com/bborbe/semantic-search
cd semantic-search
# Install dev dependencies
make install
# Run checks
make check
# Run tests
make test
```
## License
BSD 2-Clause License — see [LICENSE](LICENSE).
TDQS
A4/5.0
Scored across 3 tools
Disambiguation5/5
Each tool serves a distinct purpose: semantic search, duplicate detection, and content retrieval. There is no overlap or ambiguity between them.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern (search_related, check_duplicates, get_content) using snake_case. The naming is predictable and readable.
Tool Count5/5
Three tools is an appropriate, focused set for a semantic search server. Each tool earns its place and the scope is well-defined.
Completeness5/5
The server covers the core needs of semantic search: finding related notes, detecting duplicates, and retrieving content. There are no missing operations that would hinder its stated purpose.
Maintenance
ActivityMaintained
ResponsivenessNo issues