QMD
QMD는 로컬 디스크의 문서를 색인하고 검색하는 작은 RAG 엔진이다. 외부 API를 쓰지 않고 전부 로컬에서 처리된다.
검색은 BM25(키워드) + 벡터(의미) + 재순위(rerank) 세 가지를 조합한다. MCP 서버로 띄우면 LLM이 내 문서를 직접 조회할 수 있다.
[Files] → Embedding → [SQLite] → Hybrid Search → MCP → LLM민감한 코드베이스도 외부로 나가지 않는다.
설치
공통 (macOS / Linux / Windows)
npm install -g @tobilu/qmd
qmd collection add .
qmd embed
qmd query "test" -n 1 # 최초 실행 시 GGUF 모델 약 2.2GB 내려받음MCP 설정 (~/.claude.json 등):
{
"mcpServers": {
"qmd": { "type": "stdio", "command": "qmd", "args": ["mcp"] }
}
}맥·리눅스는 여기서 끝.
Windows 예외
현재 Windows에서는 npm install -g만으로는 qmd 명령이 바로 동작하지 않는다. npm이 생성한 래퍼가 실제 스크립트 진입점을 제대로 가리키지 않기 때문이다. 아래 PowerShell로 해결한다.
npm install -g @tobilu/qmd
$npmDir = "$env:APPDATA\npm"
@'
@echo off
node "%~dp0\node_modules\@tobilu\qmd\dist\cli\qmd.js" %*
'@ | Set-Content "$npmDir\qmd.cmd" -Encoding ASCII
@'
#!/usr/bin/env pwsh
node "$PSScriptRoot\node_modules\@tobilu\qmd\dist\cli\qmd.js" @args
'@ | Set-Content "$npmDir\qmd.ps1" -Encoding UTF8MCP 설정도 Windows에서는 command를 "qmd" 대신 "node" + 스크립트 절대 경로로 지정하는 편이 안정적이다.
확장자 커스터마이징
~/.config/qmd/index.yml 의 pattern을 바꾸면 .md 외의 확장자도 색인 대상이 된다.
collections:
naver-js: { path: .../intercepted_js, pattern: "**/*.js" }
ghidra-results: { path: .../ghidra/results, pattern: "**/*.json" }
wpanalyze-src: { path: .../wp-analyze, pattern: "**/*.php" }모의해킹에 쓰는 이유
LLM을 모의해킹에 활용할 때 늘 걸리는 두 가지 문제가 있다.
- 토큰 소모량이 크다 — 스캔 결과, 소스, 과거 writeup을 한꺼번에 넣으면 한 세션에 수십만 토큰이 쉽게 쌓인다
- 긴 대화에서 맥락이 유실된다 — 앞에서 찾은 취약점 흔적이 요약·절단되어 사라진다
QMD를 외부 장기 기억으로 쓰면 디스크의 파일이 기준점이 된다. 관련된 조각만 꺼내와서 한 질의당 1~5k 토큰으로 유지된다.
실측: 난독화 번들 JS
naver.com에서 수집한 beautify된 번들 JS 세 개를 대상으로, "세션/상태 관리 코드 찾기"를 세 가지 방식으로 비교했다.
대상 파일
| 파일 | 라인 | 크기 | 전체 로드 시 토큰 |
|---|---|---|---|
main.10379089.js | 38,753 | 1.43 MB | 약 350k |
search.d9f69b84.js | 13,632 | 542 KB | 약 130k |
preload.e5e2ade4.js | 8,382 | 301 KB | 약 75k |
결과 (정확한 심볼명을 모르는 실전 상황)
| 방법 | 토큰 | 한 줄 평 |
|---|---|---|
전체 Read | 약 350k+ | 맥락 창 포화 |
Grep + Read | 사실상 불가 | "auth" 80+건, "token" 140+건 — 오탐 폭주 |
| QMD MCP | 약 800 | 의미 기반 검색, 번들 여러 개를 한 번에 순위화 |
HyDE 질의 하나로 실제 받은 응답:
{
"results": [
{ "docid": "#40365e", "file": "main-10379089.js", "score": 0.88 },
{ "docid": "#e5ef04", "file": "search-d9f69b84.js", "score": 0.50 },
{ "docid": "#d60d5e", "file": "preload-e5e2ade4.js", "score": 0.38 }
]
}이어서 get(#40365e) 한 번만으로 StorageManager = { reset, setStateByKey, ... } 구조와 window.ntm.push(...) 추적 코드가 바로 확인됐다. 토큰으로 보면 약 400배 절감.
난독화로 변수명이 e, t, n 같은 한 글자로 치환되어 있어도, 주변 문자열과 API 호출 패턴 덕에 의미 검색이 제대로 붙는다. Grep 방식으로는 흉내 내기 어려운 지점이다.
다음 편 예고: MCP를 변형해 결과 본문 대신 저장된 파일 경로만 돌려주도록 만들면 토큰을 한층 더 아낄 수 있다. LLM이 경로만 받아두고 필요할 때 읽어가는 구조. 이 방식으로 개조한 Ghidra MCP 사례는 별도 글에서 다룰 예정이다.