hwp-rag-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hwp_rag_mcp-0.1.0/.gitignore +12 -0
- hwp_rag_mcp-0.1.0/CHANGELOG.md +13 -0
- hwp_rag_mcp-0.1.0/LICENSE +22 -0
- hwp_rag_mcp-0.1.0/PKG-INFO +191 -0
- hwp_rag_mcp-0.1.0/README.md +152 -0
- hwp_rag_mcp-0.1.0/pyproject.toml +87 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/__init__.py +35 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/__main__.py +6 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/cli.py +67 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/config.py +73 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/embeddings.py +26 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/index.py +689 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/models.py +73 -0
- hwp_rag_mcp-0.1.0/src/hwp_rag_mcp/server.py +88 -0
- hwp_rag_mcp-0.1.0/tests/conftest.py +47 -0
- hwp_rag_mcp-0.1.0/tests/test_cli.py +19 -0
- hwp_rag_mcp-0.1.0/tests/test_config.py +27 -0
- hwp_rag_mcp-0.1.0/tests/test_embeddings.py +36 -0
- hwp_rag_mcp-0.1.0/tests/test_index.py +179 -0
- hwp_rag_mcp-0.1.0/tests/test_mcp_stdio.py +47 -0
- hwp_rag_mcp-0.1.0/tests/test_real_documents.py +87 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Local HWP/HWPX parsing, element-aware chunking, multilingual E5 embeddings, and FAISS search.
|
|
10
|
+
- Safe FAISS and JSON persistence without pickle deserialization.
|
|
11
|
+
- Explicit index status/sync CLI and MCP tools for Codex and Claude Code.
|
|
12
|
+
- Korean-first setup and operations guide.
|
|
13
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HWP RAG MCP Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hwp-rag-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local HWP/HWPX retrieval for Codex and Claude Code over MCP
|
|
5
|
+
Project-URL: Homepage, https://github.com/jaypakdevkr/HWP-RAG-MCP
|
|
6
|
+
Project-URL: Repository, https://github.com/jaypakdevkr/HWP-RAG-MCP
|
|
7
|
+
Project-URL: Issues, https://github.com/jaypakdevkr/HWP-RAG-MCP/issues
|
|
8
|
+
Author: HWP RAG MCP Contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: faiss,hwp,hwpx,korean,langchain,mcp,rag
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
22
|
+
Requires-Python: <3.14,>=3.10
|
|
23
|
+
Requires-Dist: faiss-cpu<2,>=1.12
|
|
24
|
+
Requires-Dist: langchain-community<1,>=0.4
|
|
25
|
+
Requires-Dist: langchain-huggingface<2,>=1
|
|
26
|
+
Requires-Dist: langchain-hwp-hwpx-loader<0.2,>=0.1.1
|
|
27
|
+
Requires-Dist: langchain-text-splitters<2,>=1
|
|
28
|
+
Requires-Dist: mcp<2,>=1.20
|
|
29
|
+
Requires-Dist: platformdirs<5,>=4
|
|
30
|
+
Requires-Dist: sentence-transformers<6,>=5
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
33
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
37
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# HWP RAG MCP
|
|
41
|
+
|
|
42
|
+
`hwp-rag-mcp`는 바탕화면의 `.hwp`/`.hwpx` 문서를 로컬에서 파싱하고 검색할 수 있게
|
|
43
|
+
해 주는 MCP 서버입니다. 문서는 로컬 E5 임베딩과 FAISS로 색인되며, Codex 또는 Claude
|
|
44
|
+
Code가 검색 근거를 받아 최종 답변을 작성합니다.
|
|
45
|
+
|
|
46
|
+
- 외부 문서 API나 OpenAI API 키가 필요하지 않습니다.
|
|
47
|
+
- 문서 내용과 벡터 인덱스는 사용자 컴퓨터에만 저장됩니다.
|
|
48
|
+
- 표, 각주, 미주, 메모의 LangChain 메타데이터를 유지합니다.
|
|
49
|
+
- 파일 변경 후 사용자가 명시적으로 동기화해야 하므로 검색 동작이 예측 가능합니다.
|
|
50
|
+
|
|
51
|
+
> 현재 버전은 개인용·소규모 데모 문서함을 대상으로 합니다. OCR, 이미지 검색, 자동 파일
|
|
52
|
+
> 감시, 원격 MCP 전송, 서버 내부 답변 생성은 지원하지 않습니다.
|
|
53
|
+
|
|
54
|
+
## 1. 준비
|
|
55
|
+
|
|
56
|
+
Python 3.10~3.13과 [uv](https://docs.astral.sh/uv/) 사용을 권장합니다.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mkdir -p ~/Desktop/dataset
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
검색할 HWP/HWPX 파일을 `~/Desktop/dataset`에 넣습니다. 하위 폴더도 재귀적으로
|
|
63
|
+
검색합니다. 심볼릭 링크와 다른 확장자는 무시합니다.
|
|
64
|
+
|
|
65
|
+
## 2. 최초 인덱스 생성
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp sync \
|
|
69
|
+
--dataset-dir ~/Desktop/dataset
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
최초 실행에서는 `intfloat/multilingual-e5-small` 모델과 로컬 추론 의존성을 다운로드하므로
|
|
73
|
+
시간과 디스크 공간이 필요합니다. 이후 모델 캐시와 인덱스가 유지되는 동안에는 오프라인
|
|
74
|
+
검색이 가능합니다.
|
|
75
|
+
|
|
76
|
+
상태 확인과 강제 재구축:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp status \
|
|
80
|
+
--dataset-dir ~/Desktop/dataset
|
|
81
|
+
|
|
82
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp sync \
|
|
83
|
+
--dataset-dir ~/Desktop/dataset --force
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--dataset-dir`를 생략하면 `HWP_RAG_DATASET_DIR` 환경변수, 그다음
|
|
87
|
+
`~/Desktop/dataset`을 사용합니다.
|
|
88
|
+
|
|
89
|
+
## 3. Codex 연결
|
|
90
|
+
|
|
91
|
+
CLI에서 로컬 STDIO MCP 서버를 등록합니다.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
codex mcp add hwp-rag -- \
|
|
95
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp serve \
|
|
96
|
+
--dataset-dir ~/Desktop/dataset
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
직접 설정할 경우 `~/.codex/config.toml`에 다음을 추가할 수 있습니다. MCP 프로세스가 셸을
|
|
100
|
+
거치지 않는 환경을 고려하면 절대경로가 가장 안전합니다.
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[mcp_servers.hwp-rag]
|
|
104
|
+
command = "uvx"
|
|
105
|
+
args = [
|
|
106
|
+
"--from", "hwp-rag-mcp",
|
|
107
|
+
"hwp-rag-mcp", "serve",
|
|
108
|
+
"--dataset-dir", "/Users/your-name/Desktop/dataset",
|
|
109
|
+
]
|
|
110
|
+
tool_timeout_sec = 600
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
등록 후 `codex mcp list` 또는 Codex의 `/mcp` 메뉴에서 연결 상태를 확인합니다.
|
|
114
|
+
|
|
115
|
+
## 4. Claude Code 연결
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
claude mcp add hwp-rag -- \
|
|
119
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp serve \
|
|
120
|
+
--dataset-dir ~/Desktop/dataset
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
프로젝트 단위 설정이 필요하면 Claude Code의 `--scope project` 옵션을 함께 사용합니다.
|
|
124
|
+
|
|
125
|
+
## 5. MCP 도구
|
|
126
|
+
|
|
127
|
+
| 도구 | 용도 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `get_index_status` | 인덱스가 `missing`, `current`, `stale`인지 확인 |
|
|
130
|
+
| `sync_index` | 파일 변경 후 전체 인덱스를 명시적으로 재구축 |
|
|
131
|
+
| `list_documents` | 마지막 유효 인덱스에 포함된 문서 목록 확인 |
|
|
132
|
+
| `search_documents` | 질문과 유사한 근거 청크 검색 |
|
|
133
|
+
|
|
134
|
+
예시 요청:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
HWP 문서에서 연차휴가 신청 조건을 검색하고, 근거 파일명을 함께 표시해 줘.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`search_documents`는 최대 20개 결과를 반환하며, 정확한 파일명 목록으로 검색 범위를 제한할
|
|
141
|
+
수 있습니다. 파일이 변경되면 오래된 인덱스로 검색하지 않고 먼저 `sync_index`를 요구합니다.
|
|
142
|
+
|
|
143
|
+
## 인덱스와 개인정보
|
|
144
|
+
|
|
145
|
+
- FAISS 인덱스는 운영체제별 사용자 데이터 디렉터리에 저장됩니다.
|
|
146
|
+
- 데이터셋 절대경로의 SHA-256 일부를 저장 폴더 키로 사용합니다.
|
|
147
|
+
- `index.faiss`, `documents.json`, `manifest.json`만 저장하며 pickle 역직렬화는 사용하지
|
|
148
|
+
않습니다.
|
|
149
|
+
- manifest에는 파일 크기, 수정 시각, SHA-256과 모델·분할 설정이 기록됩니다.
|
|
150
|
+
- MCP 도구는 서버 시작 시 지정한 데이터셋 밖의 파일 경로를 입력받지 않습니다.
|
|
151
|
+
- API 키를 텍스트 파일로 저장하거나 읽는 기능은 없습니다.
|
|
152
|
+
|
|
153
|
+
## 문제 해결
|
|
154
|
+
|
|
155
|
+
### `index_missing`
|
|
156
|
+
|
|
157
|
+
데이터셋 경로와 파일 확장자를 확인한 뒤 CLI의 `sync`를 실행합니다.
|
|
158
|
+
|
|
159
|
+
### `index_stale`
|
|
160
|
+
|
|
161
|
+
파일이 추가·수정·삭제되었거나 분할 설정이 달라졌습니다. `sync` 또는 MCP의
|
|
162
|
+
`sync_index`를 실행합니다.
|
|
163
|
+
|
|
164
|
+
### 최초 동기화가 MCP에서 시간 초과됨
|
|
165
|
+
|
|
166
|
+
모델 다운로드가 포함된 최초 동기화는 터미널의 `hwp-rag-mcp sync`로 먼저 완료합니다.
|
|
167
|
+
Codex 설정에서는 필요하면 `tool_timeout_sec`를 늘립니다.
|
|
168
|
+
|
|
169
|
+
### 암호화 또는 손상 문서
|
|
170
|
+
|
|
171
|
+
해당 파일은 동기화 보고서의 `skipped_files` 또는 `failed_files`에 표시되며, 다른 정상
|
|
172
|
+
문서는 계속 색인됩니다.
|
|
173
|
+
|
|
174
|
+
## 개발
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m venv .venv
|
|
178
|
+
source .venv/bin/activate
|
|
179
|
+
python -m pip install -e '.[dev]'
|
|
180
|
+
ruff check .
|
|
181
|
+
pytest
|
|
182
|
+
python -m build
|
|
183
|
+
twine check dist/*
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## English summary
|
|
187
|
+
|
|
188
|
+
HWP RAG MCP builds a local, explicitly synchronized FAISS index from Korean HWP/HWPX files and
|
|
189
|
+
exposes evidence retrieval over MCP. It uses a local multilingual E5 model, requires no embedding API
|
|
190
|
+
key, and leaves answer generation to Codex or Claude Code.
|
|
191
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# HWP RAG MCP
|
|
2
|
+
|
|
3
|
+
`hwp-rag-mcp`는 바탕화면의 `.hwp`/`.hwpx` 문서를 로컬에서 파싱하고 검색할 수 있게
|
|
4
|
+
해 주는 MCP 서버입니다. 문서는 로컬 E5 임베딩과 FAISS로 색인되며, Codex 또는 Claude
|
|
5
|
+
Code가 검색 근거를 받아 최종 답변을 작성합니다.
|
|
6
|
+
|
|
7
|
+
- 외부 문서 API나 OpenAI API 키가 필요하지 않습니다.
|
|
8
|
+
- 문서 내용과 벡터 인덱스는 사용자 컴퓨터에만 저장됩니다.
|
|
9
|
+
- 표, 각주, 미주, 메모의 LangChain 메타데이터를 유지합니다.
|
|
10
|
+
- 파일 변경 후 사용자가 명시적으로 동기화해야 하므로 검색 동작이 예측 가능합니다.
|
|
11
|
+
|
|
12
|
+
> 현재 버전은 개인용·소규모 데모 문서함을 대상으로 합니다. OCR, 이미지 검색, 자동 파일
|
|
13
|
+
> 감시, 원격 MCP 전송, 서버 내부 답변 생성은 지원하지 않습니다.
|
|
14
|
+
|
|
15
|
+
## 1. 준비
|
|
16
|
+
|
|
17
|
+
Python 3.10~3.13과 [uv](https://docs.astral.sh/uv/) 사용을 권장합니다.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
mkdir -p ~/Desktop/dataset
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
검색할 HWP/HWPX 파일을 `~/Desktop/dataset`에 넣습니다. 하위 폴더도 재귀적으로
|
|
24
|
+
검색합니다. 심볼릭 링크와 다른 확장자는 무시합니다.
|
|
25
|
+
|
|
26
|
+
## 2. 최초 인덱스 생성
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp sync \
|
|
30
|
+
--dataset-dir ~/Desktop/dataset
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
최초 실행에서는 `intfloat/multilingual-e5-small` 모델과 로컬 추론 의존성을 다운로드하므로
|
|
34
|
+
시간과 디스크 공간이 필요합니다. 이후 모델 캐시와 인덱스가 유지되는 동안에는 오프라인
|
|
35
|
+
검색이 가능합니다.
|
|
36
|
+
|
|
37
|
+
상태 확인과 강제 재구축:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp status \
|
|
41
|
+
--dataset-dir ~/Desktop/dataset
|
|
42
|
+
|
|
43
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp sync \
|
|
44
|
+
--dataset-dir ~/Desktop/dataset --force
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--dataset-dir`를 생략하면 `HWP_RAG_DATASET_DIR` 환경변수, 그다음
|
|
48
|
+
`~/Desktop/dataset`을 사용합니다.
|
|
49
|
+
|
|
50
|
+
## 3. Codex 연결
|
|
51
|
+
|
|
52
|
+
CLI에서 로컬 STDIO MCP 서버를 등록합니다.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
codex mcp add hwp-rag -- \
|
|
56
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp serve \
|
|
57
|
+
--dataset-dir ~/Desktop/dataset
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
직접 설정할 경우 `~/.codex/config.toml`에 다음을 추가할 수 있습니다. MCP 프로세스가 셸을
|
|
61
|
+
거치지 않는 환경을 고려하면 절대경로가 가장 안전합니다.
|
|
62
|
+
|
|
63
|
+
```toml
|
|
64
|
+
[mcp_servers.hwp-rag]
|
|
65
|
+
command = "uvx"
|
|
66
|
+
args = [
|
|
67
|
+
"--from", "hwp-rag-mcp",
|
|
68
|
+
"hwp-rag-mcp", "serve",
|
|
69
|
+
"--dataset-dir", "/Users/your-name/Desktop/dataset",
|
|
70
|
+
]
|
|
71
|
+
tool_timeout_sec = 600
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
등록 후 `codex mcp list` 또는 Codex의 `/mcp` 메뉴에서 연결 상태를 확인합니다.
|
|
75
|
+
|
|
76
|
+
## 4. Claude Code 연결
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
claude mcp add hwp-rag -- \
|
|
80
|
+
uvx --from hwp-rag-mcp hwp-rag-mcp serve \
|
|
81
|
+
--dataset-dir ~/Desktop/dataset
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
프로젝트 단위 설정이 필요하면 Claude Code의 `--scope project` 옵션을 함께 사용합니다.
|
|
85
|
+
|
|
86
|
+
## 5. MCP 도구
|
|
87
|
+
|
|
88
|
+
| 도구 | 용도 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `get_index_status` | 인덱스가 `missing`, `current`, `stale`인지 확인 |
|
|
91
|
+
| `sync_index` | 파일 변경 후 전체 인덱스를 명시적으로 재구축 |
|
|
92
|
+
| `list_documents` | 마지막 유효 인덱스에 포함된 문서 목록 확인 |
|
|
93
|
+
| `search_documents` | 질문과 유사한 근거 청크 검색 |
|
|
94
|
+
|
|
95
|
+
예시 요청:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
HWP 문서에서 연차휴가 신청 조건을 검색하고, 근거 파일명을 함께 표시해 줘.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`search_documents`는 최대 20개 결과를 반환하며, 정확한 파일명 목록으로 검색 범위를 제한할
|
|
102
|
+
수 있습니다. 파일이 변경되면 오래된 인덱스로 검색하지 않고 먼저 `sync_index`를 요구합니다.
|
|
103
|
+
|
|
104
|
+
## 인덱스와 개인정보
|
|
105
|
+
|
|
106
|
+
- FAISS 인덱스는 운영체제별 사용자 데이터 디렉터리에 저장됩니다.
|
|
107
|
+
- 데이터셋 절대경로의 SHA-256 일부를 저장 폴더 키로 사용합니다.
|
|
108
|
+
- `index.faiss`, `documents.json`, `manifest.json`만 저장하며 pickle 역직렬화는 사용하지
|
|
109
|
+
않습니다.
|
|
110
|
+
- manifest에는 파일 크기, 수정 시각, SHA-256과 모델·분할 설정이 기록됩니다.
|
|
111
|
+
- MCP 도구는 서버 시작 시 지정한 데이터셋 밖의 파일 경로를 입력받지 않습니다.
|
|
112
|
+
- API 키를 텍스트 파일로 저장하거나 읽는 기능은 없습니다.
|
|
113
|
+
|
|
114
|
+
## 문제 해결
|
|
115
|
+
|
|
116
|
+
### `index_missing`
|
|
117
|
+
|
|
118
|
+
데이터셋 경로와 파일 확장자를 확인한 뒤 CLI의 `sync`를 실행합니다.
|
|
119
|
+
|
|
120
|
+
### `index_stale`
|
|
121
|
+
|
|
122
|
+
파일이 추가·수정·삭제되었거나 분할 설정이 달라졌습니다. `sync` 또는 MCP의
|
|
123
|
+
`sync_index`를 실행합니다.
|
|
124
|
+
|
|
125
|
+
### 최초 동기화가 MCP에서 시간 초과됨
|
|
126
|
+
|
|
127
|
+
모델 다운로드가 포함된 최초 동기화는 터미널의 `hwp-rag-mcp sync`로 먼저 완료합니다.
|
|
128
|
+
Codex 설정에서는 필요하면 `tool_timeout_sec`를 늘립니다.
|
|
129
|
+
|
|
130
|
+
### 암호화 또는 손상 문서
|
|
131
|
+
|
|
132
|
+
해당 파일은 동기화 보고서의 `skipped_files` 또는 `failed_files`에 표시되며, 다른 정상
|
|
133
|
+
문서는 계속 색인됩니다.
|
|
134
|
+
|
|
135
|
+
## 개발
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python -m venv .venv
|
|
139
|
+
source .venv/bin/activate
|
|
140
|
+
python -m pip install -e '.[dev]'
|
|
141
|
+
ruff check .
|
|
142
|
+
pytest
|
|
143
|
+
python -m build
|
|
144
|
+
twine check dist/*
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## English summary
|
|
148
|
+
|
|
149
|
+
HWP RAG MCP builds a local, explicitly synchronized FAISS index from Korean HWP/HWPX files and
|
|
150
|
+
exposes evidence retrieval over MCP. It uses a local multilingual E5 model, requires no embedding API
|
|
151
|
+
key, and leaves answer generation to Codex or Claude Code.
|
|
152
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hwp-rag-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local HWP/HWPX retrieval for Codex and Claude Code over MCP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.14"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "HWP RAG MCP Contributors" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["hwp", "hwpx", "rag", "mcp", "faiss", "langchain", "korean"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Text Processing :: Indexing",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"faiss-cpu>=1.12,<2",
|
|
31
|
+
"langchain-community>=0.4,<1",
|
|
32
|
+
"langchain-huggingface>=1,<2",
|
|
33
|
+
"langchain-hwp-hwpx-loader>=0.1.1,<0.2",
|
|
34
|
+
"langchain-text-splitters>=1,<2",
|
|
35
|
+
"mcp>=1.20,<2",
|
|
36
|
+
"platformdirs>=4,<5",
|
|
37
|
+
"sentence-transformers>=5,<6",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"build>=1.2",
|
|
43
|
+
"mypy>=1.11",
|
|
44
|
+
"pytest>=8",
|
|
45
|
+
"pytest-asyncio>=0.24",
|
|
46
|
+
"ruff>=0.7",
|
|
47
|
+
"twine>=5",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.scripts]
|
|
51
|
+
hwp-rag-mcp = "hwp_rag_mcp.cli:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://github.com/jaypakdevkr/HWP-RAG-MCP"
|
|
55
|
+
Repository = "https://github.com/jaypakdevkr/HWP-RAG-MCP"
|
|
56
|
+
Issues = "https://github.com/jaypakdevkr/HWP-RAG-MCP/issues"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.wheel]
|
|
59
|
+
packages = ["src/hwp_rag_mcp"]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.sdist]
|
|
62
|
+
include = [
|
|
63
|
+
"/src",
|
|
64
|
+
"/tests",
|
|
65
|
+
"/README.md",
|
|
66
|
+
"/LICENSE",
|
|
67
|
+
"/CHANGELOG.md",
|
|
68
|
+
"/pyproject.toml",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
addopts = "-ra"
|
|
74
|
+
asyncio_mode = "auto"
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 100
|
|
78
|
+
target-version = "py310"
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
82
|
+
|
|
83
|
+
[tool.mypy]
|
|
84
|
+
python_version = "3.12"
|
|
85
|
+
strict = true
|
|
86
|
+
packages = ["hwp_rag_mcp"]
|
|
87
|
+
warn_unused_ignores = false
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Local HWP/HWPX retrieval over the Model Context Protocol."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
# Set this before importing FAISS/LangChain modules, which may transitively import
|
|
7
|
+
# Hugging Face tokenizers. Parallel tokenizers are unnecessary for this local STDIO
|
|
8
|
+
# process and have caused shutdown crashes on some macOS ARM Python builds.
|
|
9
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("hwp-rag-mcp")
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover - source tree without installation
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
from .index import IndexManager
|
|
17
|
+
from .models import (
|
|
18
|
+
DocumentSummary,
|
|
19
|
+
IndexStatus,
|
|
20
|
+
SearchResponse,
|
|
21
|
+
SearchResult,
|
|
22
|
+
SyncFailure,
|
|
23
|
+
SyncReport,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DocumentSummary",
|
|
28
|
+
"IndexManager",
|
|
29
|
+
"IndexStatus",
|
|
30
|
+
"SearchResponse",
|
|
31
|
+
"SearchResult",
|
|
32
|
+
"SyncFailure",
|
|
33
|
+
"SyncReport",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Command-line interface for status, synchronization, and MCP serving."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .index import IndexManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _add_dataset_argument(parser: argparse.ArgumentParser) -> None:
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--dataset-dir",
|
|
17
|
+
type=Path,
|
|
18
|
+
default=None,
|
|
19
|
+
help="HWP/HWPX folder (CLI > HWP_RAG_DATASET_DIR > ~/Desktop/dataset)",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="hwp-rag-mcp",
|
|
26
|
+
description="Build and serve a local HWP/HWPX FAISS retrieval index.",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
29
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
30
|
+
|
|
31
|
+
status_parser = subparsers.add_parser("status", help="Check index freshness")
|
|
32
|
+
_add_dataset_argument(status_parser)
|
|
33
|
+
|
|
34
|
+
sync_parser = subparsers.add_parser("sync", help="Explicitly rebuild the index")
|
|
35
|
+
_add_dataset_argument(sync_parser)
|
|
36
|
+
sync_parser.add_argument("--force", action="store_true", help="Rebuild a current index")
|
|
37
|
+
|
|
38
|
+
serve_parser = subparsers.add_parser("serve", help="Run the MCP STDIO server")
|
|
39
|
+
_add_dataset_argument(serve_parser)
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
44
|
+
parser = build_parser()
|
|
45
|
+
args = parser.parse_args(argv)
|
|
46
|
+
|
|
47
|
+
if args.command == "serve":
|
|
48
|
+
from .server import run_server
|
|
49
|
+
|
|
50
|
+
run_server(args.dataset_dir)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
manager = IndexManager(args.dataset_dir)
|
|
55
|
+
if args.command == "status":
|
|
56
|
+
print(manager.status().model_dump_json(indent=2))
|
|
57
|
+
return 0
|
|
58
|
+
if args.command == "sync":
|
|
59
|
+
report = manager.sync(force=args.force)
|
|
60
|
+
print(report.model_dump_json(indent=2))
|
|
61
|
+
return 0 if report.state == "current" else 1
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
print(f"{exc.__class__.__name__}: {exc}", file=sys.stderr)
|
|
64
|
+
return 1
|
|
65
|
+
parser.error(f"Unsupported command: {args.command}")
|
|
66
|
+
return 2
|
|
67
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Configuration and path resolution for the local RAG service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from platformdirs import user_data_path
|
|
11
|
+
|
|
12
|
+
DEFAULT_MODEL_NAME = "intfloat/multilingual-e5-small"
|
|
13
|
+
DEFAULT_CHUNK_SIZE = 1_000
|
|
14
|
+
DEFAULT_CHUNK_OVERLAP = 150
|
|
15
|
+
DATASET_ENV_VAR = "HWP_RAG_DATASET_DIR"
|
|
16
|
+
STORAGE_ENV_VAR = "HWP_RAG_STORAGE_DIR"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_path(value: str | Path) -> Path:
|
|
20
|
+
"""Expand and resolve a user-provided path without requiring it to exist."""
|
|
21
|
+
|
|
22
|
+
return Path(value).expanduser().resolve(strict=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_dataset_dir(value: str | Path | None = None) -> Path:
|
|
26
|
+
"""Resolve dataset path using CLI value, environment, then Desktop default."""
|
|
27
|
+
|
|
28
|
+
if value is not None:
|
|
29
|
+
return normalize_path(value)
|
|
30
|
+
environment_value = os.getenv(DATASET_ENV_VAR)
|
|
31
|
+
if environment_value:
|
|
32
|
+
return normalize_path(environment_value)
|
|
33
|
+
return normalize_path(Path.home() / "Desktop" / "dataset")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_storage_root(value: str | Path | None = None) -> Path:
|
|
37
|
+
"""Resolve the private application data directory used for indexes."""
|
|
38
|
+
|
|
39
|
+
if value is not None:
|
|
40
|
+
return normalize_path(value)
|
|
41
|
+
environment_value = os.getenv(STORAGE_ENV_VAR)
|
|
42
|
+
if environment_value:
|
|
43
|
+
return normalize_path(environment_value)
|
|
44
|
+
return normalize_path(user_data_path("hwp-rag-mcp", appauthor=False))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def dataset_storage_key(dataset_dir: Path) -> str:
|
|
48
|
+
"""Create a stable, non-sensitive storage key for an absolute dataset path."""
|
|
49
|
+
|
|
50
|
+
digest = hashlib.sha256(str(dataset_dir).encode("utf-8")).hexdigest()[:16]
|
|
51
|
+
return f"dataset-{digest}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class IndexConfig:
|
|
56
|
+
"""Immutable settings that affect the generated vector index."""
|
|
57
|
+
|
|
58
|
+
dataset_dir: Path
|
|
59
|
+
storage_root: Path
|
|
60
|
+
model_name: str = DEFAULT_MODEL_NAME
|
|
61
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
62
|
+
chunk_overlap: int = DEFAULT_CHUNK_OVERLAP
|
|
63
|
+
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
if self.chunk_size <= 0:
|
|
66
|
+
raise ValueError("chunk_size must be greater than zero")
|
|
67
|
+
if self.chunk_overlap < 0 or self.chunk_overlap >= self.chunk_size:
|
|
68
|
+
raise ValueError("chunk_overlap must be non-negative and smaller than chunk_size")
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def index_dir(self) -> Path:
|
|
72
|
+
return self.storage_root / dataset_storage_key(self.dataset_dir)
|
|
73
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Local multilingual embedding construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from langchain_core.embeddings import Embeddings
|
|
8
|
+
|
|
9
|
+
from .config import DEFAULT_MODEL_NAME
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_local_embeddings(model_name: str = DEFAULT_MODEL_NAME) -> Embeddings:
|
|
13
|
+
"""Create the supported local E5 embedder with retrieval prompts enabled."""
|
|
14
|
+
|
|
15
|
+
# Hugging Face tokenizers can initialize worker resources that are unnecessary for
|
|
16
|
+
# this single-process STDIO server and unstable in some macOS ARM Python builds.
|
|
17
|
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
|
18
|
+
|
|
19
|
+
from langchain_huggingface import HuggingFaceEmbeddings
|
|
20
|
+
|
|
21
|
+
return HuggingFaceEmbeddings(
|
|
22
|
+
model_name=model_name,
|
|
23
|
+
model_kwargs={"device": "cpu"},
|
|
24
|
+
encode_kwargs={"normalize_embeddings": True, "prompt": "passage: "},
|
|
25
|
+
query_encode_kwargs={"normalize_embeddings": True, "prompt": "query: "},
|
|
26
|
+
)
|