QMD - Query Markdown

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.

Category
访问服务器

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

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.json with 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:

  1. Temporary Containers: Each MCP tool call = new container with --rm flag (auto-cleanup)
  2. Named Volume (qmd-cache): 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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选