claude-code-hwp-mcp 0.2.0

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.
Files changed (34) hide show
  1. package/README.md +409 -0
  2. package/dist/hwp-bridge.d.ts +67 -0
  3. package/dist/hwp-bridge.js +320 -0
  4. package/dist/hwpx-engine.d.ts +39 -0
  5. package/dist/hwpx-engine.js +187 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +54 -0
  8. package/dist/prompts/hwp-prompts.d.ts +2 -0
  9. package/dist/prompts/hwp-prompts.js +368 -0
  10. package/dist/resources/document-resources.d.ts +3 -0
  11. package/dist/resources/document-resources.js +109 -0
  12. package/dist/server.d.ts +12 -0
  13. package/dist/server.js +29 -0
  14. package/dist/tools/analysis-tools.d.ts +4 -0
  15. package/dist/tools/analysis-tools.js +414 -0
  16. package/dist/tools/composite-tools.d.ts +3 -0
  17. package/dist/tools/composite-tools.js +664 -0
  18. package/dist/tools/document-tools.d.ts +3 -0
  19. package/dist/tools/document-tools.js +264 -0
  20. package/dist/tools/editing-tools.d.ts +4 -0
  21. package/dist/tools/editing-tools.js +916 -0
  22. package/package.json +31 -0
  23. package/python/__pycache__/hwp_analyzer.cpython-313.pyc +0 -0
  24. package/python/__pycache__/hwp_editor.cpython-313.pyc +0 -0
  25. package/python/__pycache__/hwp_service.cpython-313.pyc +0 -0
  26. package/python/__pycache__/privacy_scanner.cpython-313.pyc +0 -0
  27. package/python/__pycache__/ref_reader.cpython-313.pyc +0 -0
  28. package/python/__pycache__/test_integration.cpython-313.pyc +0 -0
  29. package/python/hwp_analyzer.py +544 -0
  30. package/python/hwp_editor.py +933 -0
  31. package/python/hwp_service.py +1291 -0
  32. package/python/privacy_scanner.py +115 -0
  33. package/python/ref_reader.py +115 -0
  34. package/python/requirements.txt +2 -0
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { HwpBridge } from '../hwp-bridge.js';
3
+ export declare function registerDocumentTools(server: McpServer, bridge: HwpBridge): void;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Document lifecycle tools: list, open, close, save
3
+ */
4
+ import { z } from 'zod';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ const HWP_EXTENSIONS = new Set(['.hwp', '.hwpx']);
8
+ const SKIP_DIRS = new Set(['node_modules', '.git', '__pycache__', 'dist', '.next']);
9
+ const MAX_SCAN_DEPTH = 10;
10
+ function formatKoreanDate(date) {
11
+ const y = date.getFullYear();
12
+ const m = date.getMonth() + 1;
13
+ const d = date.getDate();
14
+ const h = String(date.getHours()).padStart(2, '0');
15
+ const min = String(date.getMinutes()).padStart(2, '0');
16
+ return `${y}년 ${m}월 ${d}일 ${h}:${min}`;
17
+ }
18
+ function listHwpFiles(directory, recursive) {
19
+ const results = [];
20
+ const visited = new Set();
21
+ function scan(dir, depth) {
22
+ if (depth > MAX_SCAN_DEPTH)
23
+ return;
24
+ let realDir;
25
+ try {
26
+ realDir = fs.realpathSync(dir);
27
+ }
28
+ catch {
29
+ return;
30
+ }
31
+ if (visited.has(realDir))
32
+ return;
33
+ visited.add(realDir);
34
+ let entries;
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ const fullPath = path.join(dir, entry.name);
43
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
44
+ try {
45
+ const stat = fs.statSync(fullPath);
46
+ if (stat.isDirectory()) {
47
+ if (recursive && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
48
+ scan(fullPath, depth + 1);
49
+ }
50
+ continue;
51
+ }
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ }
57
+ if (entry.isDirectory())
58
+ continue;
59
+ const ext = path.extname(entry.name).toLowerCase();
60
+ if (!HWP_EXTENSIONS.has(ext))
61
+ continue;
62
+ try {
63
+ const stat = fs.statSync(fullPath);
64
+ results.push({
65
+ path: fullPath,
66
+ name: entry.name,
67
+ size: stat.size,
68
+ modifiedAt: stat.mtime.toISOString(),
69
+ modifiedAtKR: formatKoreanDate(stat.mtime),
70
+ });
71
+ }
72
+ catch {
73
+ // skip inaccessible files
74
+ }
75
+ }
76
+ }
77
+ scan(directory, 0);
78
+ return results;
79
+ }
80
+ export function registerDocumentTools(server, bridge) {
81
+ // ── 환경 진단 도구 (Python/pyhwpx/한글 없이도 동작) ──
82
+ server.tool('hwp_check_setup', '사용 환경을 진단합니다. Python, pyhwpx, 한글 프로그램의 설치 여부를 확인하고 미설치 항목의 설치 방법을 안내합니다. 처음 사용하거나 에러 발생 시 이 도구를 먼저 호출하세요.', {}, async () => {
83
+ try {
84
+ const prereq = await bridge.checkPrerequisites();
85
+ const items = [];
86
+ if (!prereq.os.ok) {
87
+ items.push(`❌ OS: ${prereq.os.error}`);
88
+ }
89
+ if (prereq.python.found) {
90
+ items.push(`✅ Python ${prereq.python.version}`);
91
+ }
92
+ else {
93
+ items.push(`❌ Python 미설치\n ${prereq.python.guide}`);
94
+ }
95
+ if (prereq.pyhwpx.found) {
96
+ items.push(`✅ pyhwpx ${prereq.pyhwpx.version || ''}`);
97
+ }
98
+ else if (prereq.python.found) {
99
+ items.push(`❌ pyhwpx 미설치\n ${prereq.pyhwpx.guide}`);
100
+ }
101
+ if (prereq.hwp.found) {
102
+ items.push('✅ 한글(HWP) 프로그램');
103
+ }
104
+ else if (prereq.pyhwpx.found) {
105
+ items.push(`❌ 한글(HWP) 미설치\n ${prereq.hwp.guide}`);
106
+ }
107
+ return { content: [{ type: 'text', text: JSON.stringify({
108
+ status: prereq.ok ? 'ready' : 'not_ready',
109
+ message: prereq.ok
110
+ ? '모든 요구사항이 충족되었습니다. HWP 도구를 사용할 수 있습니다.'
111
+ : '아래 항목을 설치한 후 다시 시도하세요.',
112
+ details: prereq,
113
+ summary: items.join('\n'),
114
+ }) }] };
115
+ }
116
+ catch (err) {
117
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
118
+ }
119
+ });
120
+ server.tool('hwp_list_files', '디렉토리 내 HWP/HWPX 파일 목록을 반환합니다. Python/한글 프로그램 없이도 사용 가능합니다. 문서 작업 전 파일 위치를 확인할 때 먼저 호출하세요.', {
121
+ directory: z.string().optional().describe('탐색할 디렉토리 경로 (기본: 현재 디렉토리)'),
122
+ recursive: z.boolean().optional().describe('하위 디렉토리 재귀 탐색 여부 (기본: false)'),
123
+ }, async ({ directory, recursive }) => {
124
+ const dir = directory ? path.resolve(directory) : process.cwd();
125
+ if (!fs.existsSync(dir)) {
126
+ return { content: [{ type: 'text', text: JSON.stringify({ error: `디렉토리를 찾을 수 없습니다: ${dir}` }) }], isError: true };
127
+ }
128
+ const files = listHwpFiles(dir, recursive ?? false);
129
+ return {
130
+ content: [{ type: 'text', text: JSON.stringify({ directory: dir, files, total: files.length }) }],
131
+ };
132
+ });
133
+ server.tool('hwp_open_document', '지정된 경로의 HWP/HWPX 파일을 열어 편집 준비합니다. 이미 열린 문서가 있으면 자동으로 닫고 새 문서를 엽니다. 문서를 열면 hwp_analyze_document로 구조를 파악하세요.', {
134
+ file_path: z.string().describe('HWP/HWPX 파일의 절대 또는 상대 경로'),
135
+ }, async ({ file_path }) => {
136
+ const resolved = path.resolve(file_path);
137
+ if (!fs.existsSync(resolved)) {
138
+ return { content: [{ type: 'text', text: JSON.stringify({ error: `파일을 찾을 수 없습니다: ${resolved}` }) }], isError: true };
139
+ }
140
+ const ext = path.extname(resolved).toLowerCase();
141
+ if (!HWP_EXTENSIONS.has(ext)) {
142
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'HWP 또는 HWPX 파일만 지원합니다.' }) }], isError: true };
143
+ }
144
+ try {
145
+ await bridge.ensureRunning();
146
+ // P1 #9: 이미 열린 문서가 있으면 먼저 닫기
147
+ if (bridge.getCurrentDocument()) {
148
+ try {
149
+ await bridge.send('close_document', {});
150
+ }
151
+ catch { /* Python이 죽었을 수 있으므로 무시 */ }
152
+ bridge.setCurrentDocument(null);
153
+ bridge.setCachedAnalysis(null);
154
+ }
155
+ const response = await bridge.send('open_document', { file_path: resolved }, 60000);
156
+ if (!response.success) {
157
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
158
+ }
159
+ bridge.setCurrentDocument(resolved);
160
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
161
+ }
162
+ catch (err) {
163
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
164
+ }
165
+ });
166
+ server.tool('hwp_close_document', '현재 열린 HWP 문서를 닫습니다. 다른 문서를 열기 전이나 작업 완료 후 호출하세요.', async () => {
167
+ if (!bridge.getCurrentDocument()) {
168
+ return { content: [{ type: 'text', text: JSON.stringify({
169
+ error: '열린 문서가 없습니다.',
170
+ hint: 'Python 프로세스가 재시작되면 열린 문서 상태가 초기화됩니다.',
171
+ }) }], isError: true };
172
+ }
173
+ try {
174
+ await bridge.ensureRunning();
175
+ const response = await bridge.send('close_document', {});
176
+ if (!response.success) {
177
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
178
+ }
179
+ bridge.setCurrentDocument(null);
180
+ bridge.setCachedAnalysis(null);
181
+ return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', message: '문서를 닫았습니다.' }) }] };
182
+ }
183
+ catch (err) {
184
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
185
+ }
186
+ });
187
+ server.tool('hwp_save_document', '현재 열린 문서를 지정된 경로와 형식으로 저장합니다. 편집 작업 후 반드시 호출하여 변경사항을 저장하세요.', {
188
+ path: z.string().describe('저장할 파일 경로'),
189
+ format: z.enum(['hwp', 'hwpx', 'pdf', 'docx']).optional().describe('저장 형식 (생략 시 경로 확장자에서 추론, 기본: hwp)'),
190
+ }, async ({ path: savePath, format }) => {
191
+ if (!bridge.getCurrentDocument()) {
192
+ return {
193
+ content: [{ type: 'text', text: JSON.stringify({
194
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
195
+ hint: 'Python 프로세스가 재시작되면 열린 문서 상태가 초기화됩니다.',
196
+ }) }],
197
+ isError: true,
198
+ };
199
+ }
200
+ const resolved = path.resolve(savePath);
201
+ const dir = path.dirname(resolved);
202
+ if (!fs.existsSync(dir)) {
203
+ return { content: [{ type: 'text', text: JSON.stringify({ error: `저장 디렉토리가 존재하지 않습니다: ${dir}` }) }], isError: true };
204
+ }
205
+ // P1 #6: 경로가 디렉토리인지 확인
206
+ try {
207
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
208
+ return { content: [{ type: 'text', text: JSON.stringify({ error: `저장 경로가 디렉토리입니다. 파일 이름을 포함한 경로를 지정하세요: ${resolved}` }) }], isError: true };
209
+ }
210
+ }
211
+ catch { /* stat 실패는 무시 — 파일이 아직 없을 수 있음 */ }
212
+ // P1 #6: format 미지정 시 확장자에서 추론
213
+ let saveFormat = format;
214
+ if (!saveFormat) {
215
+ const ext = path.extname(resolved).toLowerCase().replace('.', '');
216
+ if (['hwp', 'hwpx', 'pdf', 'docx'].includes(ext)) {
217
+ saveFormat = ext;
218
+ }
219
+ else {
220
+ saveFormat = 'hwp';
221
+ }
222
+ }
223
+ // P1 #6: 확장자가 format과 불일치 시 자동 추가
224
+ let finalPath = resolved;
225
+ const currentExt = path.extname(resolved).toLowerCase().replace('.', '');
226
+ if (currentExt !== saveFormat) {
227
+ finalPath = `${resolved}.${saveFormat}`;
228
+ }
229
+ try {
230
+ await bridge.ensureRunning();
231
+ const response = await bridge.send('save_as', {
232
+ path: finalPath,
233
+ format: saveFormat,
234
+ });
235
+ if (!response.success) {
236
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
237
+ }
238
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
239
+ }
240
+ catch (err) {
241
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
242
+ }
243
+ });
244
+ // ── PDF 전용 내보내기 ──
245
+ server.tool('hwp_export_pdf', '현재 문서를 PDF로 내보냅니다. "PDF로 변환해줘" 요청에 사용하세요.', {
246
+ output_path: z.string().describe('PDF 저장 경로 (예: C:/output/문서.pdf)'),
247
+ }, async ({ output_path }) => {
248
+ if (!bridge.getCurrentDocument()) {
249
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
250
+ }
251
+ try {
252
+ await bridge.ensureRunning();
253
+ const resolved = path.resolve(output_path);
254
+ const response = await bridge.send('save_as', { path: resolved, format: 'pdf' }, 60000);
255
+ if (!response.success) {
256
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
257
+ }
258
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
259
+ }
260
+ catch (err) {
261
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
262
+ }
263
+ });
264
+ }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { HwpBridge } from '../hwp-bridge.js';
3
+ import type { Toolset } from '../server.js';
4
+ export declare function registerEditingTools(server: McpServer, bridge: HwpBridge, toolset?: Toolset): void;