paddleocr
Reads text from scanned pages, photos and PDFs and returns it as structured Markdown or JSON, so you can pull tables, formulas and headers out of a document without retyping them. Covers many languages and works as input for AI tools.
Searches local markdown notes, docs, and wikis on your device using keywords or natural language. Lets Claude Code find notes, retrieve documents, and answer from indexed markdown.
Claude Code
> /plugin marketplace add tobi/qmd> /plugin install qmd@qmd
Codex
$ npx skills add tobi/qmd -a codex
Cursor
$ npx skills add tobi/qmd -a cursor
Gemini CLI
$ npx skills add tobi/qmd -a gemini-cli
GitHub Copilot
$ copilot plugin marketplace add tobi/qmd$ copilot plugin install qmd@qmd
OpenCode
$ npx skills add tobi/qmd -a opencode
Windsurf
$ npx skills add tobi/qmd -a windsurf
An on-device search engine for everything you need to remember. Index your markdown notes, meeting transcripts, documentation, and knowledge bases. Search with keywords or natural language. Ideal for your agentic flows.
QMD combines BM25 full-text search, vector semantic search, and LLM re-ranking—all running locally via node-llama-cpp with GGUF models.
flowchart LR
Q[User Query] --> X[Query Expansion]
Q --> FTS[BM25 Search]
Q --> VS[Vector Search]
X --> HYDE[HyDE]
X --> VEC[Vec dense sentences]
X --> LEX[Lex BM25 keywords]
HYDE --> VS
VEC --> VS
LEX --> FTS
VS --> RRF[Reciprocal Rank Fusion]
FTS --> RRF
RRF --> RR[LLM Reranker]
RR --> OUT[Final ranked results]
Typed expansions are routed exclusively: lex → BM25/FTS, vec and hyde → vector search. The original query is sent to both backends, then fused with RRF and reranked.
You can read more about QMD's progress in the CHANGELOG.
# Install globally (Node or Bun)
npm install -g @tobilu/qmd
# or
bun install -g @tobilu/qmd
# Or run directly
npx @tobilu/qmd ...
bunx @tobilu/qmd ...
# Create collections for your notes, docs, and meeting transcripts
qmd collection add ~/notes --name notes
qmd collection add ~/Documents/meetings --name meetings
qmd collection add ~/work/docs --name docs
# Add context to help with search results, each piece of context will be returned when matching sub documents are returned. This works as a tree. This is the key feature of QMD as it allows LLMs to make much better contextual choices when selecting documents. Don't sleep on it!
qmd context add qmd://notes "Personal notes and ideas"
qmd context add qmd://meetings "Meeting transcripts and notes"
qmd context add qmd://docs "Work documentation"
# Generate embeddings for semantic search
qmd embed
# Search across everything
qmd search "project timeline" # Fast keyword search
qmd vsearch "how to deploy" # Semantic search
qmd query "quarterly planning process" # Hybrid + reranking (best quality)
# Get a specific document
qmd get "meetings/2024-01-15.md"
# Get a document by docid (shown in search results)
qmd get "#abc123"
# Get multiple documents by glob pattern
qmd multi-get "journals/2025-05*.md"
# Search within a specific collection
qmd search "API" -c notes
# Export all matches for an agent
qmd search "API" --all --files --min-score 0.3
QMD's --json and --files output formats are designed for agentic workflows:
# Get structured results for an LLM
qmd search "authentication" --json -n 10
# List all relevant files above a threshold
qmd query "error handling" --all --files --min-score 0.4
# Retrieve full document content
qmd get "docs/api-reference.md" --full
Although the tool works perfectly fine when you just tell your agent to use it on the command line, it also exposes an MCP (Model Context Protocol) server for tighter integration.
Tools exposed:
query — Search with typed sub-queries (lex/vec/hyde), combined via RRF + rerankingget — Retrieve a document by path or docid (with fuzzy matching suggestions)multi_get — Batch retrieve by glob pattern, comma-separated list, or docidsstatus — Index health and collection info, including each collection's top metadata keysmetadata — Discover metadata keys, types, and value counts to filter onClaude Desktop configuration (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"qmd": {
"command": "qmd",
"args": ["mcp"]
}
}
}
Claude Code — Install the plugin (recommended):
claude plugin marketplace add tobi/qmd
claude plugin install qmd@qmd
Or configure MCP manually in ~/.claude/settings.json:
{
"mcpServers": {
"qmd": {
"command": "qmd",
"args": ["mcp"]
}
}
}
By default, QMD's MCP server uses stdio (launched as a subprocess by each client). For a shared, long-lived server that avoids repeated model loading, use the HTTP transport:
# Foreground (Ctrl-C to stop)
qmd mcp --http # localhost:8181
qmd mcp --http --port 8080 # custom port
qmd mcp --http --host 0.0.0.0 # bind all interfaces (e.g. container probes)
# Background daemon
qmd mcp --http --daemon # start, writes PID to ~/.cache/qmd/mcp.pid
qmd mcp stop # stop via PID file
qmd status # shows "MCP: running (PID ...)" when active
The server binds to localhost by default. Pass --host (or set the QMD_HOST
environment variable) to override — --host 0.0.0.0 is useful when the server
runs in a container and a liveness probe connects from a non-loopback address.
The HTTP server exposes two endpoints:
POST /mcp — MCP Streamable HTTP (JSON responses, stateless)POST /query (alias /search) — structured search without the MCP protocol. Accepts the same optional filter object as the query tool (invalid filters return 400); see Metadata FilteringPOST /metadata — metadata discovery without the MCP protocol. Same body as the metadata tool (invalid filters return 400); see Metadata DiscoveryGET /health — liveness check with uptimeEvery request is screened before routing: a request carrying an Origin header
that does not name a loopback address is rejected with 403, as is a Host
header naming something other than the address the server is bound to. This is
what stops a web page you visit from reading your index through DNS rebinding —
loopback binding alone does not, since the browser makes the request from your
own machine.
Requests without an Origin header — curl, MCP clients, editors — are
unaffected, which covers every normal local client.
| Variable | Effect |
|---|---|
QMD_ALLOWED_ORIGINS | Comma-separated origins to accept in addition to loopback, e.g. https://notes.internal. Set to * to disable the check entirely. |
QMD_ALLOWED_HOSTS | Comma-separated Host values to accept in addition to loopback and the bind address. |
--host 0.0.0.0 cannot know which Host values are legitimate, so it skips the
host check and warns at startup. Set QMD_ALLOWED_HOSTS to re-enable it, and
remember the endpoints are unauthenticated — put your own auth in front of a
server that is reachable off-host.
LLM models stay loaded in VRAM across requests. Embedding/reranking contexts are disposed after 5 min idle and transparently recreated on the next request (~1s penalty, models remain loaded).
Point any MCP client at http://localhost:8181/mcp to connect.
| Tool | Parameter | Type | Notes |
|---|---|---|---|
query | searches | array | Typed sub-queries (lex/vec/hyde), 1–10. Required. First gets 2x weight. |
query | collections | string[] | Filter by collection names (OR). Array only — singular collection is silently ignored. |
query | filter | object | Metadata filter (recursive operator-discriminated JSON AST; see Metadata Filtering) |
query | intent | string | Disambiguation context (does not search on its own) |
query | limit | number | Max results (default 10) |
query | minScore | number | Minimum relevance 0–1 (default 0) |
query | candidateLimit | number | Max candidates to rerank (default 40) |
query | rerank | boolean | Run LLM reranking (default true); set false for RRF-only |
get | file | string | Path, docid (#abc123), or path:from:count (e.g. #abc123:120:40) |
get | fromLine | number | Start line (1-indexed); overrides the :from suffix |
get | maxLines | number | Limit returned lines |
get | lineNumbers | boolean | Prefix lines with numbers (default true) |
multi_get | pattern | string | Glob pattern or comma-separated list |
multi_get | maxBytes | number | Skip files larger than N (default 10240) |
multi_get | maxLines | number | Limit lines per file |
multi_get | lineNumbers | boolean | Prefix lines with numbers (default true) |
metadata | collections | string[] | Restrict discovery to collection names (default: the collections query searches) |
metadata | match | object | Report only metadata entries matching this condition (same AST as filter, with field naming the entry's key or value) |
metadata | filter | object | Count only documents matching this filter (same AST as query) |
metadata | keyLimit | number | Keys reported (default 50). totalKeys and remainingKeys describe the rest |
metadata | keyOffset | number | Keys skipped before the window, in report order (default 0) |
metadata | valueLimit | number | Values reported per key and type (default 10). remainingValues reports the rest |
metadata | valueOffset | number | Values skipped per key and type before the window, in sort order (default 0) |
metadata | sort | string | count (default) or value |
metadata | minCount | number | Hide values held by fewer documents (default 1) |
Unknown parameters are silently ignored (not rejected) — double-check names if
results seem unscoped. The HTTP /query and /search endpoints return
qmd://collection/path URIs in the file field, matching the CLI and MCP output.
Use QMD as a library in your own Node.js or Bun applications.
npm install @tobilu/qmd
import { createStore } from '@tobilu/qmd'
const store = await createStore({
dbPath: './my-index.sqlite',
config: {
collections: {
docs: { path: '/path/to/docs', pattern: '**/*.md' },
},
},
})
const results = await store.search({ query: "authentication flow" })
console.log(results.map(r => `${r.title} (${Math.round(r.score * 100)}%)`))
await store.close()
createStore() accepts three modes:
import { createStore } from '@tobilu/qmd'
// 1. Inline config — no files needed besides the DB
const store = await createStore({
dbPath: './index.sqlite',
config: {
collections: {
docs: { path: '/path/to/docs', pattern: '**/*.md' },
notes: { path: '/path/to/notes' },
},
},
})
// 2. YAML config file — collections defined in a file
const store2 = await createStore({
dbPath: './index.sqlite',
configPath: './qmd.yml',
})
// 3. DB-only — reopen a previously configured store
const store3 = await createStore({ dbPath: './index.sqlite' })
The unified search() method handles both simple queries and pre-expanded structured queries:
Reads text from scanned pages, photos and PDFs and returns it as structured Markdown or JSON, so you can pull tables, formulas and headers out of a document without retyping them. Covers many languages and works as input for AI tools.
Gives your AI assistant a memory that persists across sessions, so it remembers your preferences and earlier work in this repository instead of starting fresh. Search, forget, pause and resume memories with simple commands.
Gives AI agents a library of ready-made skills for scientific work such as metabolic flux analysis, protein experiment design, time series modeling, and analytical method validation, so research tasks need less hand-built code. It works with any agent that supports the open Agent Skills standard.
Gives Claude Code skills, agents, and commands for PostHog analytics, session replay, feature flags, error tracking, and LLM observability. Includes MCP server setup and project access checks.
Install it with the command on its page. It includes qmd, release. Its skills do not fire on their own yet. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it