MCP Hub
Back to servers

QMD - Query Markdown

A containerized hybrid search server that combines BM25 keyword matching and vector semantic search to index and query local markdown knowledge bases.

Tools
5
Updated
Jan 12, 2026

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

ToolDescription
qmd_queryHybrid search combining BM25 keyword + vector semantic search
qmd_vsearchVector-only semantic search for conceptual similarity
qmd_refresh_indexTrigger ingestion pipeline for new/modified files
qmd_getRetrieve full content of a specific file
qmd_listList 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 PathPurposeExample Host Path
/app/kbYour markdown files (read-only)~/Knowledge_Base
/root/.cache/qmdSQLite DB & embeddings cacheDocker 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

VariableDefaultDescription
OPENROUTER_API_KEY(required)OpenRouter API key for embeddings
QMD_EMBEDDING_MODELopenai/text-embedding-3-smallEmbedding model to use
MCP_TRANSPORTstdioTransport mode: stdio or http
QMD_PORT3000HTTP server port
QMD_KB_PATH/app/kbKnowledge base path inside container
QMD_CACHE_PATH/root/.cache/qmdCache directory for SQLite DB
QMD_CHUNK_SIZE500Tokens per chunk
QMD_CHUNK_OVERLAP50Overlap 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)

ItemCost
text-embedding-3-small~$0.02 per 1M tokens
Initial indexing (100 docs)< $0.01
Per-query cost~$0.000002 (negligible)

License

MIT

Reviews

No reviews yet

Sign in to write a review