QMD - Query Markdown
Provides hybrid search over local markdown knowledge bases using BM25 keyword search and vector semantic search. Enables indexing, querying, and retrieving markdown documentation with dual-mode support for local and remote agents.
README
QMD - Query Markdown
A containerized MCP (Model Context Protocol) server that provides hybrid search over your local markdown knowledge base. Works with Claude Code, Claude Desktop, Cursor, and other MCP-compatible agents.
Features
- Dual-mode communication: STDIO for local agents (Claude Code), HTTP/SSE for remote agents
- Stateless STDIO architecture: Temporary containers (
docker run --rm) with persistent volume storage - Hybrid search: Combines BM25 keyword search and vector semantic search with RRF fusion
- Vector embeddings: OpenRouter API for high-quality embeddings (text-embedding-3-small)
- SQLite persistence: FTS5 for keyword search, BLOB storage for vectors in named Docker volume
- Zero-config deployment: Automated setup script (
setup-qmd-mcp.sh) configures everything - No long-running containers: For STDIO mode, containers auto-remove after each tool call
Deployment Modes
STDIO Mode (Claude Code, MCP Agents)
- Architecture: Stateless temporary containers
- Lifecycle: Container created per tool call, auto-removed after execution
- Persistence: Named Docker volume (
qmd-cache) stores SQLite DB + embeddings - Setup: Run
./setup-qmd-mcp.shor manually configure~/.claude.json - Use case: Local development with Claude Code or other MCP-compatible agents
HTTP Mode (Remote Agents, Web Services)
- Architecture: Long-running persistent container
- Lifecycle: Managed via
docker compose up/down - Persistence: Host directory mount via
.envconfiguration - Setup: Configure
.envand rundocker compose up -d - Use case: Remote agents, web services, or when you need HTTP/SSE transport
Quick Start
STDIO Mode (Recommended for Claude Code)
# 1. Build the Docker image
docker compose build
# 2. Run the automated setup script
./setup-qmd-mcp.sh
# 3. Restart Claude Code
# 4. Start using QMD!
# In Claude Code: "Please index my markdown files using qmd"
The setup script will:
- ✓ Clean up old containers/volumes
- ✓ Configure
~/.claude.jsonwith correct MCP settings - ✓ Verify Docker image and notes path
- ✓ Test embeddings are enabled
See Claude Code Integration via STDIO for details.
HTTP Mode (For Remote Agents)
1. Configure Environment
cd qmd
# Copy the example environment file
cp .env.example .env
# Edit .env and add your OpenRouter API key
# Get your key at: https://openrouter.ai/keys
.env file contents:
# Required: OpenRouter API key for embeddings
OPENROUTER_API_KEY=sk-or-v1-your-key-here
# Optional: Embedding model (default shown)
QMD_EMBEDDING_MODEL=openai/text-embedding-3-small
# Optional: Knowledge base path on host
QMD_KB_PATH=./kb
# Optional: Cache path for SQLite DB
QMD_CACHE_PATH=./data
2. Build the Image
docker compose build
3. Run HTTP Server
# Start the server
docker compose up -d
# Verify it's running
curl http://localhost:3000/health
# {"status":"ok","mode":"http"}
# View logs
docker compose logs -f qmd
# Stop
docker compose down
4. Test STDIO Mode (Optional)
Test the MCP server directly before configuring Claude Code:
# Send a proper initialize message
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' | \
docker run -i --rm \
-e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
-v /path/to/your/notes:/app/kb:ro \
-v qmd-cache:/root/.cache/qmd \
qmd:latest mcp
Expected output includes: "serverInfo":{"name":"qmd","version":"0.1.0"} and Embeddings: enabled
Claude Code Integration via STDIO
QMD uses a stateless container architecture for MCP/STDIO mode. Each tool call launches a fresh temporary container that executes and auto-removes (--rm). Persistence is achieved through a named Docker volume.
Deployment Architecture
┌─────────────────────────────────────────────────┐
│ Claude Code │
│ ↓ (launches on each MCP tool call) │
│ docker run -i --rm ... │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ Temporary QMD Container (auto-removes) │ │
│ │ │ │
│ │ /app/kb ← Volume: Your markdown files │ │
│ │ /root/.cache/qmd ← Volume: qmd-cache │ │
│ │ │ │
│ │ [SQLite DB + Embeddings] → Persists! │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Key Components:
- Temporary Containers: Each MCP tool call = new container with
--rmflag (auto-cleanup) - Named Volume (
qmd-cache): Persists SQLite database and embeddings across all container runs - Read-Only Mount: Your markdown files mounted at
/app/kb:ro(read-only) - Stateless Design: No long-running containers, all state in the persistent volume
Setup with Automated Script
Use the provided setup script for automatic configuration:
./setup-qmd-mcp.sh
This will:
- Clean up old containers and volumes
- Update
~/.claude.jsonwith correct MCP configuration - Verify Docker image and notes path
- Test embeddings are enabled
After running, restart Claude Code to load the new configuration.
Manual Configuration
Edit ~/.claude.json and add the QMD MCP server:
{
"mcpServers": {
"qmd": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"OPENROUTER_API_KEY=sk-or-v1-your-key-here",
"-v",
"/path/to/your/markdown/notes:/app/kb:ro",
"-v",
"qmd-cache:/root/.cache/qmd",
"qmd:latest",
"mcp"
]
}
}
}
Important:
- Replace
sk-or-v1-your-key-herewith your OpenRouter API key - Replace
/path/to/your/markdown/noteswith your actual notes directory - The API key must be in
argsvia-eflag (not a separateenvsection) - After editing, restart Claude Code for changes to take effect
Volume Persistence
The qmd-cache named volume ensures your indexed data persists:
# Check volume exists
docker volume ls | grep qmd-cache
# Inspect volume contents
docker run --rm -v qmd-cache:/cache qmd:latest ls -lh /cache/
# Verify database
docker run --rm -v qmd-cache:/cache qmd:latest \
sqlite3 /cache/qmd.db "SELECT COUNT(*) FROM documents;"
Data persists across:
- Container restarts
- Docker daemon restarts
- System reboots
To start fresh:
docker volume rm qmd-cache
MCP Tools
| Tool | Description |
|---|---|
qmd_query |
Hybrid search combining BM25 keyword + vector semantic search |
qmd_vsearch |
Vector-only semantic search for conceptual similarity |
qmd_refresh_index |
Trigger ingestion pipeline for new/modified files |
qmd_get |
Retrieve full content of a specific file |
qmd_list |
List all indexed files in the knowledge base |
Usage Examples
Ingestion: Index Your Knowledge Base
After adding or modifying markdown files, trigger the ingestion pipeline:
You: "I just added new documentation files. Please index them."
Claude: [Calls qmd_refresh_index tool]
MCP Tool Call:
{
"name": "qmd_refresh_index",
"arguments": {
"force": false
}
}
Response:
{
"message": "Ingestion complete",
"stats": {
"new": 5,
"updated": 2,
"unchanged": 10,
"deleted": 0,
"totalChunks": 245
}
}
Force re-index all files:
You: "Please re-index everything from scratch"
Claude: [Calls qmd_refresh_index with force=true]
Hybrid Search: Find Relevant Content
Combines keyword matching (BM25) with semantic similarity (vectors) using RRF fusion:
You: "Search for information about API authentication"
Claude: [Calls qmd_query tool]
MCP Tool Call:
{
"name": "qmd_query",
"arguments": {
"query": "API authentication OAuth JWT tokens",
"limit": 5
}
}
Response:
{
"results": [
{
"path": "docs/security/authentication.md",
"score": 0.89,
"excerpt": "## Authentication Methods\n\nOur API supports multiple authentication methods:\n- OAuth 2.0 with PKCE\n- JWT bearer tokens\n- API keys for server-to-server..."
},
{
"path": "docs/api/endpoints.md",
"score": 0.72,
"excerpt": "### Authorization Header\n\nAll API requests require authentication via the Authorization header..."
}
]
}
Semantic Search: Conceptual Similarity
Use vector-only search when looking for conceptually related content:
You: "Find documents about handling errors gracefully"
Claude: [Calls qmd_vsearch tool]
MCP Tool Call:
{
"name": "qmd_vsearch",
"arguments": {
"query": "graceful error handling recovery patterns",
"limit": 5
}
}
Response:
{
"results": [
{
"path": "docs/patterns/resilience.md",
"score": 0.85,
"excerpt": "## Circuit Breaker Pattern\n\nWhen a service fails repeatedly, the circuit breaker opens to prevent cascading failures..."
},
{
"path": "docs/api/error-codes.md",
"score": 0.78,
"excerpt": "## Retry Strategies\n\nImplement exponential backoff with jitter for transient failures..."
}
]
}
Retrieve Full Document
Get the complete content of a specific file:
You: "Show me the full content of the authentication docs"
Claude: [Calls qmd_get tool]
MCP Tool Call:
{
"name": "qmd_get",
"arguments": {
"path": "docs/security/authentication.md"
}
}
Response:
{
"path": "docs/security/authentication.md",
"content": "# Authentication\n\n## Overview\n\nOur API uses OAuth 2.0..."
}
List All Indexed Files
See what's in your knowledge base:
You: "What files are in my knowledge base?"
Claude: [Calls qmd_list tool]
MCP Tool Call:
{
"name": "qmd_list",
"arguments": {}
}
Response:
{
"files": [
"docs/api/endpoints.md",
"docs/api/error-codes.md",
"docs/security/authentication.md",
"docs/patterns/resilience.md",
"notes/meeting-2024-01-15.md"
],
"total": 5
}
Real-World Workflow Examples
Example 1: Research a topic across your notes
You: "What have I written about database performance optimization?"
Claude: [Calls qmd_query] → finds 3 relevant documents
Claude: [Calls qmd_get] → retrieves full content of most relevant
Claude: "Based on your notes, you've documented several optimization strategies..."
Example 2: Cross-reference project documentation
You: "How does our error handling compare between the API and the CLI?"
Claude: [Calls qmd_vsearch with "error handling patterns"]
Claude: "I found error handling docs for both. The API uses HTTP status codes
while the CLI uses exit codes. Both implement retry logic..."
Example 3: Find related content by concept
You: "Find anything related to making systems more reliable"
Claude: [Calls qmd_vsearch with "system reliability resilience"]
Claude: "I found documents on circuit breakers, retry strategies, health checks,
and your notes from the SRE book club..."
Volume Mappings
STDIO Mode (Claude Code)
| Container Path | Purpose | Type | Example |
|---|---|---|---|
/app/kb |
Your markdown files | Host directory (ro) | /Users/you/Notes:/app/kb:ro |
/root/.cache/qmd |
SQLite DB + embeddings | Named volume (rw) | qmd-cache:/root/.cache/qmd |
Why named volume for cache?
- Persists across all container runs
- Survives system reboots
- No filesystem permission issues
- Fast I/O performance
Why read-only for markdown files?
- Prevents accidental modifications
- Security best practice
- QMD only reads, never writes to
/app/kb
HTTP Mode (Docker Compose)
In HTTP mode, volumes are configured via .env file:
# .env file
QMD_KB_PATH=/path/to/notes # Your markdown directory
QMD_CACHE_PATH=./data # Host directory for SQLite DB
Mounting Multiple Folders (Advanced)
You can mount multiple directories into /app/kb:
# In ~/.claude.json, add multiple -v flags:
"args": [
"run", "-i", "--rm",
"-e", "OPENROUTER_API_KEY=...",
"-v", "~/Notes:/app/kb/notes:ro",
"-v", "~/Projects/docs:/app/kb/projects:ro",
"-v", "~/Research:/app/kb/research:ro",
"-v", "qmd-cache:/root/.cache/qmd",
"qmd:latest", "mcp"
]
All directories will be indexed and searchable together.
Instructing Agents to Use QMD via STDIO
For Claude Code
Once configured in ~/.claude.json, simply ask Claude naturally:
"Please index my markdown files using qmd"
"Search my notes for information about X"
"Find documents related to Y"
"List all files in my knowledge base"
Claude Code will automatically invoke the appropriate MCP tools.
For Other MCP-Compatible Agents
Any agent supporting MCP over STDIO can use QMD. Configure the agent's MCP settings with:
Command: docker
Args:
[
"run", "-i", "--rm",
"-e", "OPENROUTER_API_KEY=your-api-key",
"-v", "/path/to/notes:/app/kb:ro",
"-v", "qmd-cache:/root/.cache/qmd",
"qmd:latest", "mcp"
]
Available Tools:
qmd_list- List indexed filesqmd_refresh_index- Index/re-index filesqmd_query- Hybrid search (BM25 + vector)qmd_vsearch- Vector-only semantic searchqmd_get- Retrieve full document content
Persistence Across Sessions
The qmd-cache named volume ensures:
- Indexed documents persist between agent sessions
- Embeddings are generated once, reused forever
- No re-indexing needed unless files change
- Fast search (no cold start)
First run:
- Agent calls
qmd_refresh_index→ generates embeddings (~30 sec for 100 docs) - Agent calls
qmd_query→ instant search results
Subsequent runs:
- Agent calls
qmd_query→ instant results (no re-indexing)
Environment Variables
| Variable | Default | Description |
|---|---|---|
OPENROUTER_API_KEY |
(required) | OpenRouter API key for embeddings |
QMD_EMBEDDING_MODEL |
openai/text-embedding-3-small |
Embedding model to use |
MCP_TRANSPORT |
stdio |
Transport mode: stdio or http |
QMD_PORT |
3000 |
HTTP server port |
QMD_KB_PATH |
/app/kb |
Knowledge base path inside container |
QMD_CACHE_PATH |
/root/.cache/qmd |
Cache directory for SQLite DB |
QMD_CHUNK_SIZE |
500 |
Tokens per chunk |
QMD_CHUNK_OVERLAP |
50 |
Overlap tokens between chunks |
Docker Compose Configurations
Production (HTTP Mode)
# Uses docker-compose.yml with .env file
docker compose up -d
Development (Hot Reload)
# Combines both compose files
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
Custom Knowledge Base Path
# Override via environment or .env file
QMD_KB_PATH=/path/to/your/notes docker compose up -d
Development
Local Development (without Docker)
# Install dependencies
bun install
# Set environment variables
export OPENROUTER_API_KEY="sk-or-v1-your-key"
# Run with hot reload
bun run dev
# Build
bun run build
# Type check
bun run typecheck
Project Structure
qmd/
├── .env # Environment variables (git-ignored)
├── .env.example # Example environment file
├── docker-compose.yml # Production config
├── docker-compose.dev.yml # Development overrides
├── Dockerfile # Multi-stage build
├── entrypoint.sh # Dual-mode entrypoint
├── package.json
├── tsconfig.json
├── src/
│ ├── qmd.ts # MCP server entry point
│ ├── db.ts # SQLite schema & queries
│ ├── embeddings.ts # OpenRouter API client
│ ├── ingest.ts # Chunking & indexing pipeline
│ └── search.ts # Hybrid search with RRF
└── kb/ # Default knowledge base mount
Troubleshooting
STDIO Mode (Claude Code)
QMD tools not showing up in Claude Code
-
Check MCP configuration exists:
cat ~/.claude.json | jq '.mcpServers.qmd' -
Verify configuration has correct structure (type, command, args)
-
Restart Claude Code after any config changes
Embeddings not enabled
-
Check API key is in args (not env):
cat ~/.claude.json | jq '.mcpServers.qmd.args' | grep OPENROUTER_API_KEY -
Verify API key is valid:
curl https://openrouter.ai/api/v1/models \ -H "Authorization: Bearer sk-or-v1-your-key" -
Test container directly:
docker run --rm -e OPENROUTER_API_KEY="your-key" qmd:latest env | grep OPENROUTER
No files found / Empty knowledge base
-
Check notes path is correct in
~/.claude.json -
Verify path is accessible:
ls -la "/path/to/your/notes" -
Check files are visible in container:
docker run --rm -v "/path/to/notes:/app/kb:ro" qmd:latest ls -la /app/kb/
Index not persisting
-
Verify named volume exists:
docker volume ls | grep qmd-cache -
Check database exists in volume:
docker run --rm -v qmd-cache:/cache qmd:latest ls -lh /cache/ -
Verify data in database:
docker run --rm -v qmd-cache:/cache qmd:latest \ sqlite3 /cache/qmd.db "SELECT COUNT(*) FROM documents;"
Containers left running
This shouldn't happen with --rm flag, but check:
# Should be empty
docker ps --filter ancestor=qmd:latest
# Clean up if needed
docker ps -a --filter ancestor=qmd:latest -q | xargs docker rm -f
HTTP Mode (Docker Compose)
Container won't start
# Check logs
docker compose logs qmd
# Verify image built
docker images | grep qmd
# Check .env file
cat .env
Health check failing
# Test endpoint
curl -v http://localhost:3000/health
# Check port availability
lsof -i :3000
General Issues
Docker image not found
# Build the image
docker compose build
# Verify it exists
docker images | grep qmd
Permission issues
# Mount as read-only (STDIO mode always uses :ro)
-v ~/Knowledge_Base:/app/kb:ro
# For HTTP mode, check .env paths are accessible
ls -la "$QMD_KB_PATH"
Architecture
STDIO Mode (Claude Code)
┌─────────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ Claude Code │ │ Docker Volume (Persist) │ │
│ │ │ │ │ │
│ │ MCP Client │ │ qmd-cache:/root/.cache │ │
│ └──────┬───────┘ │ ├── qmd.db (SQLite) │ │
│ │ │ └── embeddings (BLOBs) │ │
│ │ Each tool call └────────────────────────────┘ │
│ ▼ ▲ │
│ docker run -i --rm │ │
│ │ │ Persists │
│ ▼ │ │
│ ┌─────────────────────────────────┴──────────────────┐ │
│ │ Temporary Container (auto-removes) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ MCP │──►│ Hybrid │──►│ SQLite │ │ │
│ │ │ Server │ │ Search │ │ FTS5+Vector │ │ │
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────▼─────┐ │ │
│ │ │ │ Ingest │ │ │
│ │ │ │ Pipeline │ │ │
│ │ │ └────┬─────┘ │ │
│ │ │ │ │ │
│ │ Volumes mounted: │ │ │
│ │ • /app/kb (ro) ─────┘ │ │
│ │ • /root/.cache/qmd (rw, persistent) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ Calls OpenRouter API │
│ ▼ │
│ ┌──────────────────┐ │
│ │ OpenRouter API │ │
│ │ (Embeddings) │ │
│ └──────────────────┘ │
│ │
│ ┌─────────────────────┐◄── Mounted read-only │
│ │ Your Markdown │ │
│ │ Notes Directory │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Lifecycle:
1. Claude Code launches: docker run -i --rm -v ... qmd:latest mcp
2. Container starts, loads MCP server, connects via STDIO
3. Tool executes (search/index/etc), writes to qmd-cache volume
4. Container exits and auto-removes (--rm flag)
5. Next tool call repeats 1-4, data persists in qmd-cache
HTTP Mode (Remote Agents)
For remote agents or when you need a persistent HTTP endpoint:
# Start persistent HTTP server
docker compose up -d
# Container runs continuously, listens on port 3000
# Data stored in host directory mapped via .env
HTTP mode differences:
- Long-running container (no
--rm) - HTTP/SSE transport instead of STDIO
- Managed via docker-compose
- Volume mounts from
.envconfiguration
Cost Estimate (OpenRouter)
| Item | Cost |
|---|---|
text-embedding-3-small |
~$0.02 per 1M tokens |
| Initial indexing (100 docs) | < $0.01 |
| Per-query cost | ~$0.000002 (negligible) |
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。