QMD MCP Windows 구축 및 Pentesting 활용

2026년 04월 13일
5

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 UTF8

MCP 설정도 Windows에서는 command"qmd" 대신 "node" + 스크립트 절대 경로로 지정하는 편이 안정적이다.

확장자 커스터마이징

~/.config/qmd/index.ymlpattern을 바꾸면 .md 외의 확장자도 색인 대상이 된다.

collections:
  naver-js:       { path: .../intercepted_js, pattern: "**/*.js" }
  ghidra-results: { path: .../ghidra/results, pattern: "**/*.json" }
  wpanalyze-src:  { path: .../wp-analyze,     pattern: "**/*.php" }

모의해킹에 쓰는 이유

LLM을 모의해킹에 활용할 때 늘 걸리는 두 가지 문제가 있다.

  1. 토큰 소모량이 크다 — 스캔 결과, 소스, 과거 writeup을 한꺼번에 넣으면 한 세션에 수십만 토큰이 쉽게 쌓인다
  2. 긴 대화에서 맥락이 유실된다 — 앞에서 찾은 취약점 흔적이 요약·절단되어 사라진다

QMD를 외부 장기 기억으로 쓰면 디스크의 파일이 기준점이 된다. 관련된 조각만 꺼내와서 한 질의당 1~5k 토큰으로 유지된다.


실측: 난독화 번들 JS

naver.com에서 수집한 beautify된 번들 JS 세 개를 대상으로, "세션/상태 관리 코드 찾기"를 세 가지 방식으로 비교했다.

대상 파일

파일라인크기전체 로드 시 토큰
main.10379089.js38,7531.43 MB약 350k
search.d9f69b84.js13,632542 KB약 130k
preload.e5e2ade4.js8,382301 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 사례는 별도 글에서 다룰 예정이다.