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, HTTP/SSE for remote agents
- 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
- Zero setup: Docker-based deployment with volume-mapped persistence
Quick Start
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 in HTTP Mode (Remote Agents)
# 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. Run in STDIO Mode (Claude Code)
# Test STDIO mode directly
echo '{"jsonrpc":"2.0","method":"initialize","id":1}' | \
docker run -i --rm \
-e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
-v ./kb:/app/kb:ro \
-v ./qmd-cache:/root/.cache/qmd \
qmd:latest mcp
Claude Code Integration
Add QMD to your Claude Code MCP servers configuration.
Option A: Claude Code CLI (Recommended)
# Add as global MCP server (available in all projects)
claude mcp add qmd -s user -- docker run -i --rm \
-e OPENROUTER_API_KEY="sk-or-v1-your-key-here" \
-v "$HOME/Knowledge_Base:/app/kb:ro" \
-v "qmd-cache:/root/.cache/qmd" \
qmd:latest mcp
# Or add to current project only
claude mcp add qmd -- docker run -i --rm \
-e OPENROUTER_API_KEY="sk-or-v1-your-key-here" \
-v "$HOME/Knowledge_Base:/app/kb:ro" \
-v "qmd-cache:/root/.cache/qmd" \
qmd:latest mcp
Option B: Edit ~/.claude.json
{
"mcpServers": {
"qmd": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "OPENROUTER_API_KEY=sk-or-v1-your-key-here",
"-v", "/Users/yourname/Knowledge_Base:/app/kb:ro",
"-v", "qmd-cache:/root/.cache/qmd",
"qmd:latest", "mcp"
]
}
}
}
Option C: Project-specific .mcp.json
Create .mcp.json in your project root:
{
"mcpServers": {
"qmd": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "OPENROUTER_API_KEY=sk-or-v1-your-key-here",
"-v", "${HOME}/Knowledge_Base:/app/kb:ro",
"-v", "qmd-cache:/root/.cache/qmd",
"qmd:latest", "mcp"
]
}
}
}
Manage MCP servers:
# List all configured servers
claude mcp list
# Get details for qmd
claude mcp get qmd
# Remove qmd server
claude mcp remove qmd -s user
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
| Container Path | Purpose | Example Host Path |
|---|---|---|
/app/kb | Your markdown files (read-only) | ~/Knowledge_Base |
/root/.cache/qmd | SQLite DB & embeddings cache | Docker volume qmd-cache |
Mounting Multiple Folders
docker run -i --rm \
-e OPENROUTER_API_KEY="$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
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
Container won't start
# Check logs
docker compose logs qmd
# Verify image built correctly
docker images | grep qmd
Embeddings not working
# Verify API key is set
echo $OPENROUTER_API_KEY
# Test API key directly
curl https://openrouter.ai/api/v1/models \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
Health check failing
# Test manually
curl -v http://localhost:3000/health
# Check if port is in use
lsof -i :3000
Database not persisting
Ensure you're using a named volume:
# Good - named volume persists
-v qmd-cache:/root/.cache/qmd
# Bad - anonymous volume, data lost on container removal
-v /root/.cache/qmd
Permission issues with knowledge base
# Mount as read-only to avoid permission issues
-v ~/Knowledge_Base:/app/kb:ro
Architecture
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ ┌─────────────┐ ┌─────────────────────────────────┐ │
│ │ Claude Code │ │ Docker Container (QMD) │ │
│ │ │◄─┤ ┌─────────┐ ┌────────────┐ │ │
│ │ (STDIO) │ │ │ MCP │ │ Hybrid │ │ │
│ └─────────────┘ │ │ Router │───►│ Search │ │ │
│ │ └─────────┘ └────────────┘ │ │
│ ┌─────────────┐ │ │ │ │ │
│ │ Cursor │ │ ▼ ▼ │ │
│ │ │◄─┤ ┌─────────┐ ┌────────────┐ │ │
│ │ (HTTP) │ │ │ Ingest │ │ SQLite │ │ │
│ └─────────────┘ │ │ Engine │ │ FTS5+BLOB │ │ │
│ │ └─────────┘ └────────────┘ │ │
│ ┌─────────────┐ │ │ │ │
│ │ OpenRouter │◄─┼───────┘ (embeddings API) │ │
│ │ API │ │ │ │
│ └─────────────┘ └─────────────────────────────────┘ │
│ │
│ ┌─────────────┐◄──── Volume Mount (/app/kb) │
│ │~/Knowledge │ │
│ │ _Base/ │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
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