Skip to main content
Glama
ehc-io
by ehc-io

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

# 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 ~/Knowledge_Base:/app/kb:ro \ -v qmd-cache:/root/.cache/qmd \ qmd:latest mcp

Claude Code Integration

Add QMD to your Claude Code MCP servers configuration.

# 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

-
security - not tested
F
license - not found
-
quality - not tested

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ehc-io/qmd'

If you have feedback or need assistance with the MCP directory API, please join our Discord server