Skip to main content
Glama
ehc-io

QMD - Query Markdown

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 (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.sh or 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 .env configuration

  • Setup: Configure .env and run docker compose up -d

  • Use case: Remote agents, web services, or when you need HTTP/SSE transport

Quick Start

# 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.json with correct MCP settings

  • ✓ Verify Docker image and notes path

  • ✓ Test embeddings are enabled

See


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

# 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:

  1. Temporary Containers: Each MCP tool call = new container with --rm flag (auto-cleanup)

  2. Named Volume (: Persists SQLite database and embeddings across all container runs

  3. Read-Only Mount: Your markdown files mounted at /app/kb:ro (read-only)

  4. 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.json with 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-here with your OpenRouter API key

  • Replace /path/to/your/markdown/notes with your actual notes directory

  • The API key must be in args via -e flag (not a separate env section)

  • 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 files

  • qmd_refresh_index - Index/re-index files

  • qmd_query - Hybrid search (BM25 + vector)

  • qmd_vsearch - Vector-only semantic search

  • qmd_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:

  1. Agent calls qmd_refresh_index → generates embeddings (~30 sec for 100 docs)

  2. Agent calls qmd_query → instant search results

Subsequent runs:

  1. 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

  1. Check MCP configuration exists:

    cat ~/.claude.json | jq '.mcpServers.qmd'
  2. Verify configuration has correct structure (type, command, args)

  3. Restart Claude Code after any config changes

Embeddings not enabled

  1. Check API key is in args (not env):

    cat ~/.claude.json | jq '.mcpServers.qmd.args' | grep OPENROUTER_API_KEY
  2. Verify API key is valid:

    curl https://openrouter.ai/api/v1/models \ -H "Authorization: Bearer sk-or-v1-your-key"
  3. Test container directly:

    docker run --rm -e OPENROUTER_API_KEY="your-key" qmd:latest env | grep OPENROUTER

No files found / Empty knowledge base

  1. Check notes path is correct in ~/.claude.json

  2. Verify path is accessible:

    ls -la "/path/to/your/notes"
  3. 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

  1. Verify named volume exists:

    docker volume ls | grep qmd-cache
  2. Check database exists in volume:

    docker run --rm -v qmd-cache:/cache qmd:latest ls -lh /cache/
  3. 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 .env configuration

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