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,320 @@
1
+ /**
2
+ * HWP Python Bridge for MCP Server
3
+ * Adapted from electron/services/hwp-bridge.ts — no Electron dependencies.
4
+ * All logging via console.error() to protect stdout (MCP JSON-RPC).
5
+ */
6
+ import { spawn, execFile } from 'node:child_process';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { promisify } from 'node:util';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const execFileAsync = promisify(execFile);
14
+ export class HwpBridge {
15
+ process = null;
16
+ requestId = 0;
17
+ pending = new Map();
18
+ buffer = '';
19
+ MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
20
+ startPromise = null;
21
+ pythonRunning = false;
22
+ currentDocumentPath = null;
23
+ currentDocumentFormat = null;
24
+ lastAnalysis = null;
25
+ lastError = null;
26
+ startTime = Date.now();
27
+ getPythonScriptDir() {
28
+ // npm 패키지: dist/hwp-bridge.js → ../python (패키지 내 python/)
29
+ const npmPath = path.resolve(__dirname, '../python');
30
+ if (fs.existsSync(path.join(npmPath, 'hwp_service.py')))
31
+ return npmPath;
32
+ // 개발 환경: mcp-server/dist/hwp-bridge.js → ../../python (프로젝트 루트)
33
+ return path.resolve(__dirname, '../../python');
34
+ }
35
+ findPython() {
36
+ return process.env.PYTHON_PATH || 'python';
37
+ }
38
+ async ensureRunning() {
39
+ if (this.process && this.pythonRunning)
40
+ return;
41
+ // 동시 재시작 방지: 이미 시작 중이면 기다림
42
+ if (this.startPromise)
43
+ return this.startPromise;
44
+ // Clean up previous process
45
+ if (this.process) {
46
+ this.process.kill();
47
+ this.process = null;
48
+ }
49
+ this.pythonRunning = false;
50
+ this.currentDocumentPath = null;
51
+ this.currentDocumentFormat = null;
52
+ this.lastAnalysis = null;
53
+ this.lastError = null;
54
+ this.startPromise = this.start();
55
+ try {
56
+ await this.startPromise;
57
+ }
58
+ finally {
59
+ this.startPromise = null;
60
+ }
61
+ }
62
+ async start() {
63
+ const pythonExe = this.findPython();
64
+ const scriptPath = path.join(this.getPythonScriptDir(), 'hwp_service.py');
65
+ console.error(`[HWP MCP Bridge] Starting Python: ${pythonExe} ${scriptPath}`);
66
+ this.process = spawn(pythonExe, [scriptPath], {
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ env: { ...process.env, PYTHONUNBUFFERED: '1' },
69
+ });
70
+ // Python 실행 파일 자체를 찾을 수 없는 경우 (ENOENT)
71
+ this.process.on('error', (err) => {
72
+ if (err.code === 'ENOENT') {
73
+ this.lastError = 'Python을 찾을 수 없습니다. Python 3.8+을 설치하고 PATH에 추가하세요.\n→ https://www.python.org/downloads/ (설치 시 "Add to PATH" 체크)';
74
+ }
75
+ else {
76
+ this.lastError = `Python 프로세스 시작 실패: ${err.message}`;
77
+ }
78
+ console.error('[HWP MCP Bridge] Spawn error:', err.message);
79
+ this.pythonRunning = false;
80
+ this.rejectAllPending(new Error(this.lastError));
81
+ });
82
+ this.process.stdout?.on('data', (chunk) => {
83
+ this.buffer += chunk.toString('utf-8');
84
+ // Buffer 크기 제한 (10MB) — 메모리 폭증 방지
85
+ if (this.buffer.length > this.MAX_BUFFER_SIZE) {
86
+ console.error(`[HWP MCP Bridge] Buffer exceeded ${this.MAX_BUFFER_SIZE / 1024 / 1024}MB, clearing`);
87
+ this.buffer = '';
88
+ this.rejectAllPending(new Error('응답 크기가 너무 큽니다. 문서를 닫고 다시 열어주세요.'));
89
+ return;
90
+ }
91
+ this.processBuffer();
92
+ });
93
+ this.process.stderr?.on('data', (chunk) => {
94
+ const text = chunk.toString('utf-8').trim();
95
+ console.error('[HWP MCP Bridge][stderr]', text);
96
+ if (text.includes('ModuleNotFoundError')) {
97
+ this.lastError = 'Python 모듈을 찾을 수 없습니다. pip install pyhwpx pywin32 를 실행해주세요.';
98
+ }
99
+ else if (text.includes('COM class not registered') || text.includes('CoInitialize')) {
100
+ this.lastError = '한글(HWP) 프로그램이 설치되어 있어야 합니다. 한글이 설치되어 있는지 확인하세요.';
101
+ }
102
+ else if (text.includes('RPC') || text.includes('사용할 수 없습니다')) {
103
+ this.lastError = 'RPC 서버를 사용할 수 없습니다. 한글 프로그램이 실행 중인지 확인하세요.';
104
+ }
105
+ else if (text.includes('SyntaxError') || text.includes('ImportError')) {
106
+ this.lastError = `Python 오류가 발생했습니다: ${text.split('\n').pop()}`;
107
+ }
108
+ });
109
+ this.process.on('exit', (code) => {
110
+ console.error(`[HWP MCP Bridge] Python exited with code ${code}`);
111
+ this.rejectAllPending(new Error(`Python process exited with code ${code}`));
112
+ this.process = null;
113
+ this.pythonRunning = false;
114
+ this.currentDocumentPath = null;
115
+ this.currentDocumentFormat = null;
116
+ this.lastAnalysis = null;
117
+ });
118
+ // Verify connection (with 2 retries, 5초 간격)
119
+ let lastErr;
120
+ for (let attempt = 1; attempt <= 3; attempt++) {
121
+ try {
122
+ // ping 시 Python에서 Hwp() COM 초기화 — 한글 프로그램 시작까지 최대 90초
123
+ const response = await this.send('ping', {}, 90000);
124
+ if (response.success) {
125
+ this.pythonRunning = true;
126
+ console.error('[HWP MCP Bridge] Python bridge connected');
127
+ return;
128
+ }
129
+ }
130
+ catch (err) {
131
+ lastErr = err;
132
+ console.error(`[HWP MCP Bridge] Ping attempt ${attempt}/3 failed:`, err);
133
+ if (attempt < 3) {
134
+ console.error('[HWP MCP Bridge] Retrying in 5 seconds...');
135
+ await new Promise(resolve => setTimeout(resolve, 5000));
136
+ }
137
+ }
138
+ }
139
+ // All 3 attempts failed — 단계별 안내 제공
140
+ const detail = this.lastError || '';
141
+ let hint;
142
+ if (detail.includes('Python을 찾을 수 없습니다')) {
143
+ hint = detail;
144
+ }
145
+ else if (detail.includes('RPC') || detail.includes('사용할 수 없습니다')) {
146
+ hint = '한글 프로그램이 응답하지 않습니다. 한글을 닫고 다시 시도해주세요.';
147
+ }
148
+ else if (detail.includes('ModuleNotFoundError') || detail.includes('pyhwpx')) {
149
+ hint = 'pyhwpx가 설치되지 않았습니다. 터미널에서 실행: pip install pyhwpx';
150
+ }
151
+ else if (detail.includes('COM') || detail.includes('한글')) {
152
+ hint = '한글(HWP) 프로그램이 설치되지 않았거나 COM 등록이 실패했습니다.';
153
+ }
154
+ else {
155
+ hint = 'hwp_check_setup 도구로 환경을 진단해보세요.\n 확인사항: 1) Python 3.8+ 설치 2) pip install pyhwpx 3) 한글 프로그램 설치';
156
+ }
157
+ throw new Error(`HWP MCP 시작 실패: ${hint}`);
158
+ }
159
+ processBuffer() {
160
+ let newlineIndex;
161
+ while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) {
162
+ const line = this.buffer.slice(0, newlineIndex).trim();
163
+ this.buffer = this.buffer.slice(newlineIndex + 1);
164
+ if (!line)
165
+ continue;
166
+ try {
167
+ const response = JSON.parse(line);
168
+ const pending = this.pending.get(response.id);
169
+ if (pending) {
170
+ clearTimeout(pending.timer);
171
+ this.pending.delete(response.id);
172
+ pending.resolve(response);
173
+ }
174
+ }
175
+ catch {
176
+ console.error('[HWP MCP Bridge] Invalid JSON from Python:', line);
177
+ }
178
+ }
179
+ }
180
+ async send(method, params, timeoutMs = 30000) {
181
+ // start() 내부의 ping 호출 시에는 이미 process가 있으므로 재시작 하지 않음
182
+ if (!this.process || !this.process.stdin?.writable) {
183
+ if (method !== 'ping') {
184
+ this.lastError = null;
185
+ await this.ensureRunning();
186
+ }
187
+ }
188
+ if (!this.process?.stdin?.writable) {
189
+ const detail = this.lastError || 'Python과 pyhwpx가 설치되어 있는지 확인하세요.';
190
+ throw new Error(`Python 프로세스를 시작할 수 없습니다. ${detail}`);
191
+ }
192
+ const id = `req_${++this.requestId}`;
193
+ const request = { id, method, params };
194
+ return new Promise((resolve, reject) => {
195
+ const timer = setTimeout(() => {
196
+ this.pending.delete(id);
197
+ const detail = this.lastError ? ` 원인: ${this.lastError}` : '';
198
+ reject(new Error(`요청 시간 초과 (${timeoutMs / 1000}초): ${method}.${detail}`));
199
+ }, timeoutMs);
200
+ this.pending.set(id, { resolve, reject, timer });
201
+ this.process.stdin.write(JSON.stringify(request) + '\n', (err) => {
202
+ if (err) {
203
+ clearTimeout(timer);
204
+ this.pending.delete(id);
205
+ reject(new Error(`Failed to write to Python stdin: ${err.message}`));
206
+ }
207
+ });
208
+ });
209
+ }
210
+ async shutdown() {
211
+ if (!this.process)
212
+ return;
213
+ try {
214
+ await this.send('shutdown', {}, 5000);
215
+ }
216
+ catch {
217
+ // ignore timeout on shutdown
218
+ }
219
+ this.process.kill();
220
+ this.process = null;
221
+ this.pythonRunning = false;
222
+ this.rejectAllPending(new Error('Bridge shut down'));
223
+ }
224
+ rejectAllPending(error) {
225
+ for (const [id, pending] of this.pending) {
226
+ clearTimeout(pending.timer);
227
+ pending.reject(error);
228
+ this.pending.delete(id);
229
+ }
230
+ }
231
+ // ── 사전 요구사항 체크 (Python/pyhwpx/한글) ──
232
+ async checkPrerequisites() {
233
+ const result = {
234
+ ok: false,
235
+ python: { found: false },
236
+ pyhwpx: { found: false },
237
+ hwp: { found: false },
238
+ os: { ok: process.platform === 'win32', platform: process.platform },
239
+ };
240
+ if (!result.os.ok) {
241
+ result.os.error = 'HWP MCP는 Windows 전용입니다. 한글(HWP)은 Windows COM API를 사용합니다.';
242
+ return result;
243
+ }
244
+ const pythonExe = this.findPython();
245
+ // 1) Python 체크
246
+ try {
247
+ const { stdout } = await execFileAsync(pythonExe, ['--version'], { timeout: 5000 });
248
+ const ver = stdout.trim().replace('Python ', '');
249
+ result.python = { found: true, version: ver };
250
+ }
251
+ catch {
252
+ result.python = {
253
+ found: false,
254
+ guide: 'Python 3.8+ 설치 필요\n→ https://www.python.org/downloads/\n→ 설치 시 "Add Python to PATH" 반드시 체크\n→ 설치 후 터미널 재시작',
255
+ };
256
+ return result;
257
+ }
258
+ // 2) pyhwpx 체크
259
+ try {
260
+ const { stdout } = await execFileAsync(pythonExe, ['-c', 'import pyhwpx; print(getattr(pyhwpx, "__version__", "installed"))'], { timeout: 5000 });
261
+ result.pyhwpx = { found: true, version: stdout.trim() };
262
+ }
263
+ catch {
264
+ result.pyhwpx = {
265
+ found: false,
266
+ guide: 'pyhwpx 패키지 설치 필요\n→ 터미널에서 실행: pip install pyhwpx\n→ pywin32도 함께 필요: pip install pywin32',
267
+ };
268
+ return result;
269
+ }
270
+ // 3) 한글(HWP) COM 등록 체크
271
+ try {
272
+ await execFileAsync(pythonExe, ['-c', 'import win32com.client; o = win32com.client.gencache.EnsureDispatch("HWPFrame.HwpObject"); o.XHwpDocuments.Close(False); del o'], { timeout: 15000 });
273
+ result.hwp = { found: true };
274
+ }
275
+ catch (err) {
276
+ const msg = err.message || '';
277
+ if (msg.includes('COM class not registered') || msg.includes('gencache')) {
278
+ result.hwp = {
279
+ found: false,
280
+ guide: '한글(HWP) 프로그램이 설치되지 않았습니다.\n→ 한컴오피스 한글 설치 필요 (한글 2014 이상)\n→ 설치 후 한글을 한번 실행하여 초기 설정 완료',
281
+ };
282
+ }
283
+ else {
284
+ // COM은 등록되어 있지만 다른 에러 (이미 실행 중 등) → 설치는 된 것
285
+ result.hwp = { found: true };
286
+ }
287
+ }
288
+ result.ok = result.python.found && result.pyhwpx.found && result.hwp.found;
289
+ return result;
290
+ }
291
+ // State management
292
+ getState() {
293
+ return {
294
+ pythonRunning: this.pythonRunning,
295
+ currentDocumentPath: this.currentDocumentPath,
296
+ currentDocumentFormat: this.currentDocumentFormat,
297
+ lastAnalysis: this.lastAnalysis,
298
+ uptimeMs: Date.now() - this.startTime,
299
+ };
300
+ }
301
+ setCurrentDocument(filePath) {
302
+ this.currentDocumentPath = filePath;
303
+ if (filePath) {
304
+ const ext = path.extname(filePath).toLowerCase();
305
+ this.currentDocumentFormat = ext === '.hwpx' ? 'HWPX' : 'HWP';
306
+ }
307
+ else {
308
+ this.currentDocumentFormat = null;
309
+ }
310
+ }
311
+ getCurrentDocument() {
312
+ return this.currentDocumentPath;
313
+ }
314
+ getCachedAnalysis() {
315
+ return this.lastAnalysis;
316
+ }
317
+ setCachedAnalysis(data) {
318
+ this.lastAnalysis = data;
319
+ }
320
+ }
@@ -0,0 +1,39 @@
1
+ export interface HwpxTemplate {
2
+ id: string;
3
+ name: string;
4
+ category: string;
5
+ fields: string[];
6
+ }
7
+ export declare const TEMPLATES: HwpxTemplate[];
8
+ /**
9
+ * HWPX(ZIP) 파일에서 특정 XML 파일을 읽어서 DOM으로 파싱.
10
+ */
11
+ export declare function readHwpxXml(hwpxPath: string, xmlName: string): Promise<Document>;
12
+ /**
13
+ * HWPX(ZIP) 파일의 특정 XML을 수정 후 저장.
14
+ * 기존 ZIP의 다른 파일은 그대로 유지.
15
+ */
16
+ export declare function writeHwpxXml(sourcePath: string, outputPath: string, xmlName: string, doc: Document): Promise<void>;
17
+ /**
18
+ * HWPX section XML에서 모든 텍스트를 추출.
19
+ * 경로: hp:p > hp:run > hp:t
20
+ */
21
+ export declare function extractTextFromSection(doc: Document): string[];
22
+ /**
23
+ * HWPX section XML에서 텍스트 찾아 바꾸기.
24
+ * CLAUDE.md 규칙: 수정 후 linesegarray 삭제 필수.
25
+ */
26
+ export declare function replaceTextInSection(doc: Document, find: string, replace: string): number;
27
+ /**
28
+ * 빈 HWPX 파일 생성.
29
+ * blank_template.hwpx를 복사하고, 필요시 제목 삽입.
30
+ */
31
+ export declare function createBlankHwpx(outputPath: string, title?: string): Promise<void>;
32
+ /**
33
+ * 템플릿 기반 문서 생성.
34
+ * blank_template.hwpx를 복사 → 변수 치환.
35
+ */
36
+ export declare function generateFromTemplate(templateId: string, variables: Record<string, string>, outputPath: string): Promise<{
37
+ filledFields: number;
38
+ emptyFields: string[];
39
+ }>;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * HWPX XML Engine — 한글 프로그램 없이 HWPX 파일 직접 생성/편집
3
+ *
4
+ * HWPX = ZIP(application/hwp+zip) + XML 형식
5
+ * Python execSync 제거 — Node.js jszip + @xmldom/xmldom만 사용
6
+ *
7
+ * CLAUDE.md 규칙 준수:
8
+ * - @xmldom/xmldom 사용 (fast-xml-parser 금지)
9
+ * - element.localName 사용 (tagName 금지)
10
+ * - 텍스트 수정 후 linesegarray 삭제 필수
11
+ * - charPrIDRef 변경 금지
12
+ * - 표 셀 경로: tc → subList → p → run → t
13
+ */
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
17
+ import JSZip from 'jszip';
18
+ export const TEMPLATES = [
19
+ // 공문서
20
+ { id: 'gov_official_letter', name: '공문서 (기안문/시행문)', category: '공문서', fields: ['수신자', '발신부서', '발신자', '제목', '본문', '시행일자', '문서번호'] },
21
+ { id: 'gov_report', name: '보고서', category: '공문서', fields: ['보고제목', '보고자', '부서', '보고일', '요약', '현황', '문제점', '개선방안', '기대효과'] },
22
+ { id: 'gov_draft', name: '기안문', category: '공문서', fields: ['기안자', '검토자', '결재자', '제목', '내용', '시행일', '근거'] },
23
+ { id: 'gov_minutes', name: '회의록', category: '공문서', fields: ['회의명', '일시', '장소', '참석자', '안건', '토의내용', '결정사항', '향후계획'] },
24
+ { id: 'gov_plan', name: '사업계획서', category: '공문서', fields: ['사업명', '기관명', '사업기간', '사업목적', '추진내용', '기대효과', '예산'] },
25
+ { id: 'gov_notice', name: '공고문', category: '공문서', fields: ['공고제목', '공고내용', '공고일', '기관명'] },
26
+ { id: 'gov_budget', name: '예산서', category: '공문서', fields: ['사업명', '항목', '금액', '산출근거'] },
27
+ // 기업
28
+ { id: 'biz_proposal', name: '사업제안서', category: '기업', fields: ['회사명', '제안제목', '제안배경', '제안내용', '기대효과', '일정', '예산'] },
29
+ { id: 'biz_contract', name: '계약서', category: '기업', fields: ['갑', '을', '계약명', '계약금액', '계약기간'] },
30
+ { id: 'biz_invoice', name: '견적서', category: '기업', fields: ['발행처', '수신처', '품목', '합계', '부가세', '총액'] },
31
+ { id: 'biz_meeting', name: '기업 회의록', category: '기업', fields: ['회의명', '일시', '참석자', '안건', '결정사항'] },
32
+ { id: 'biz_memo', name: '업무 메모', category: '기업', fields: ['수신', '발신', '제목', '내용'] },
33
+ { id: 'biz_mou', name: '양해각서(MOU)', category: '기업', fields: ['기관1', '기관2', '목적', '협력내용', '기간'] },
34
+ { id: 'biz_nda', name: '비밀유지계약서(NDA)', category: '기업', fields: ['갑', '을', '비밀정보범위', '기간'] },
35
+ // 학술
36
+ { id: 'academic_paper', name: '학술 논문', category: '학술', fields: ['제목', '저자', '초록', '키워드', '서론', '본론', '결론', '참고문헌'] },
37
+ { id: 'academic_report', name: '학술 보고서', category: '학술', fields: ['제목', '작성자', '과목', '내용'] },
38
+ // 개인
39
+ { id: 'personal_resume', name: '이력서', category: '개인', fields: ['이름', '생년월일', '연락처', '이메일', '학력', '경력', '자격증'] },
40
+ { id: 'personal_letter', name: '자기소개서', category: '개인', fields: ['이름', '지원분야', '성장배경', '지원동기', '입사후포부'] },
41
+ { id: 'personal_certificate', name: '증명서', category: '개인', fields: ['성명', '생년월일', '발급사유', '발급일'] },
42
+ ];
43
+ // ── HWPX 네임스페이스 ──
44
+ const NS_HP = 'http://www.hancom.co.kr/hwpml/2011/paragraph';
45
+ // ── HWPX ZIP 유틸 (Node.js jszip — Python execSync 제거) ──
46
+ /**
47
+ * HWPX(ZIP) 파일에서 특정 XML 파일을 읽어서 DOM으로 파싱.
48
+ */
49
+ export async function readHwpxXml(hwpxPath, xmlName) {
50
+ const data = fs.readFileSync(hwpxPath);
51
+ const zip = await JSZip.loadAsync(data);
52
+ const xmlFile = zip.file(xmlName);
53
+ if (!xmlFile) {
54
+ throw new Error(`HWPX 내에 ${xmlName}을 찾을 수 없습니다.`);
55
+ }
56
+ const xmlStr = await xmlFile.async('string');
57
+ const parser = new DOMParser();
58
+ return parser.parseFromString(xmlStr, 'text/xml');
59
+ }
60
+ /**
61
+ * HWPX(ZIP) 파일의 특정 XML을 수정 후 저장.
62
+ * 기존 ZIP의 다른 파일은 그대로 유지.
63
+ */
64
+ export async function writeHwpxXml(sourcePath, outputPath, xmlName, doc) {
65
+ const serializer = new XMLSerializer();
66
+ const xmlStr = serializer.serializeToString(doc);
67
+ const data = fs.readFileSync(sourcePath);
68
+ const zip = await JSZip.loadAsync(data);
69
+ zip.file(xmlName, xmlStr);
70
+ const newData = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
71
+ fs.writeFileSync(outputPath, newData);
72
+ }
73
+ // ── 텍스트 추출 ──
74
+ /**
75
+ * HWPX section XML에서 모든 텍스트를 추출.
76
+ * 경로: hp:p > hp:run > hp:t
77
+ */
78
+ export function extractTextFromSection(doc) {
79
+ const texts = [];
80
+ const paragraphs = doc.getElementsByTagNameNS(NS_HP, 'p');
81
+ for (let i = 0; i < paragraphs.length; i++) {
82
+ const p = paragraphs[i];
83
+ const runs = p.getElementsByTagNameNS(NS_HP, 'run');
84
+ let paraText = '';
85
+ for (let j = 0; j < runs.length; j++) {
86
+ const tNodes = runs[j].getElementsByTagNameNS(NS_HP, 't');
87
+ for (let k = 0; k < tNodes.length; k++) {
88
+ paraText += tNodes[k].textContent || '';
89
+ }
90
+ }
91
+ texts.push(paraText);
92
+ }
93
+ return texts;
94
+ }
95
+ // ── 텍스트 치환 ──
96
+ /**
97
+ * HWPX section XML에서 텍스트 찾아 바꾸기.
98
+ * CLAUDE.md 규칙: 수정 후 linesegarray 삭제 필수.
99
+ */
100
+ export function replaceTextInSection(doc, find, replace) {
101
+ let count = 0;
102
+ const tNodes = doc.getElementsByTagNameNS(NS_HP, 't');
103
+ for (let i = 0; i < tNodes.length; i++) {
104
+ const t = tNodes[i];
105
+ const text = t.textContent || '';
106
+ if (text.includes(find)) {
107
+ t.textContent = text.replaceAll(find, replace);
108
+ count++;
109
+ }
110
+ }
111
+ // CLAUDE.md 규칙 8: 텍스트 수정 후 linesegarray 삭제 필수
112
+ if (count > 0) {
113
+ removeLinesegarray(doc);
114
+ }
115
+ return count;
116
+ }
117
+ /**
118
+ * linesegarray 요소 삭제 (CLAUDE.md 규칙 8).
119
+ */
120
+ function removeLinesegarray(doc) {
121
+ const linesegArrays = doc.getElementsByTagNameNS(NS_HP, 'linesegarray');
122
+ const toRemove = [];
123
+ for (let i = 0; i < linesegArrays.length; i++) {
124
+ toRemove.push(linesegArrays[i]);
125
+ }
126
+ for (const el of toRemove) {
127
+ el.parentNode?.removeChild(el);
128
+ }
129
+ }
130
+ // ── 빈 HWPX 생성 ──
131
+ function getTemplatePath() {
132
+ // ESM에서 __dirname 대안
133
+ const thisFile = new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
134
+ return path.join(path.dirname(thisFile), '../../blank_template.hwpx');
135
+ }
136
+ /**
137
+ * 빈 HWPX 파일 생성.
138
+ * blank_template.hwpx를 복사하고, 필요시 제목 삽입.
139
+ */
140
+ export async function createBlankHwpx(outputPath, title) {
141
+ const templatePath = getTemplatePath();
142
+ if (!fs.existsSync(templatePath)) {
143
+ throw new Error(`빈 HWPX 템플릿을 찾을 수 없습니다: ${templatePath}. 한글에서 빈 문서를 HWPX로 저장하세요.`);
144
+ }
145
+ fs.copyFileSync(templatePath, outputPath);
146
+ if (title) {
147
+ const doc = await readHwpxXml(outputPath, 'Contents/section0.xml');
148
+ const tNodes = doc.getElementsByTagNameNS(NS_HP, 't');
149
+ if (tNodes.length > 0) {
150
+ tNodes[0].textContent = title;
151
+ removeLinesegarray(doc);
152
+ }
153
+ await writeHwpxXml(outputPath, outputPath, 'Contents/section0.xml', doc);
154
+ }
155
+ }
156
+ // ── 템플릿 생성 ──
157
+ /**
158
+ * 템플릿 기반 문서 생성.
159
+ * blank_template.hwpx를 복사 → 변수 치환.
160
+ */
161
+ export async function generateFromTemplate(templateId, variables, outputPath) {
162
+ const template = TEMPLATES.find(t => t.id === templateId);
163
+ if (!template) {
164
+ throw new Error(`템플릿을 찾을 수 없습니다: ${templateId}. hwp_template_list로 사용 가능한 템플릿을 확인하세요.`);
165
+ }
166
+ // 빈 HWPX 복사
167
+ await createBlankHwpx(outputPath);
168
+ // 변수로 텍스트 생성
169
+ const lines = [];
170
+ lines.push(template.name);
171
+ lines.push('');
172
+ for (const field of template.fields) {
173
+ const value = variables[field] || `{{${field}}}`;
174
+ lines.push(`${field}: ${value}`);
175
+ }
176
+ // section0.xml에 텍스트 삽입
177
+ const doc = await readHwpxXml(outputPath, 'Contents/section0.xml');
178
+ const tNodes = doc.getElementsByTagNameNS(NS_HP, 't');
179
+ if (tNodes.length > 0) {
180
+ tNodes[0].textContent = lines.join('\n');
181
+ removeLinesegarray(doc);
182
+ }
183
+ await writeHwpxXml(outputPath, outputPath, 'Contents/section0.xml', doc);
184
+ const filledFields = template.fields.filter(f => variables[f]).length;
185
+ const emptyFields = template.fields.filter(f => !variables[f]);
186
+ return { filledFields, emptyFields };
187
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HWP Studio MCP Server — Entry Point
4
+ * stdio transport for Claude Code integration.
5
+ * WARNING: console.log() is forbidden — stdout is MCP JSON-RPC only.
6
+ */
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { HwpBridge } from './hwp-bridge.js';
10
+ import { setupServer } from './server.js';
11
+ // --toolset 파라미터 파싱 (minimal | standard | full, 기본: standard)
12
+ const toolsetArg = process.argv.find(a => a.startsWith('--toolset'));
13
+ const toolset = toolsetArg?.split('=')[1]
14
+ ?? process.argv[process.argv.indexOf('--toolset') + 1]
15
+ ?? 'standard';
16
+ const validToolsets = ['minimal', 'standard', 'full'];
17
+ const resolvedToolset = validToolsets.includes(toolset)
18
+ ? toolset
19
+ : 'standard';
20
+ const bridge = new HwpBridge();
21
+ const server = new McpServer({
22
+ name: 'hwp-studio',
23
+ version: '0.2.0',
24
+ });
25
+ setupServer(server, bridge, resolvedToolset);
26
+ const transport = new StdioServerTransport();
27
+ await server.connect(transport);
28
+ console.error(`[HWP MCP] 서버 시작됨 (toolset: ${resolvedToolset}) — Claude Code에서 HWP 도구 사용 가능`);
29
+ // 시작 시 환경 자동 체크 (비동기, 서버 시작을 차단하지 않음)
30
+ bridge.checkPrerequisites().then(prereq => {
31
+ if (!prereq.ok) {
32
+ console.error('[HWP MCP] ⚠️ 환경 설정 필요:');
33
+ if (!prereq.os.ok)
34
+ console.error(` ❌ ${prereq.os.error}`);
35
+ if (!prereq.python.found)
36
+ console.error(' ❌ Python 미설치 → https://www.python.org/downloads/ (PATH 추가 필수)');
37
+ else if (!prereq.pyhwpx.found)
38
+ console.error(' ❌ pyhwpx 미설치 → pip install pyhwpx');
39
+ else if (!prereq.hwp.found)
40
+ console.error(' ❌ 한글(HWP) 미설치 → 한컴오피스 설치 필요');
41
+ console.error(' 💡 자세한 진단: hwp_check_setup 도구를 호출하세요');
42
+ }
43
+ else {
44
+ console.error(`[HWP MCP] ✅ 환경 준비 완료 (Python ${prereq.python.version})`);
45
+ }
46
+ }).catch(() => { });
47
+ process.on('SIGINT', async () => {
48
+ await bridge.shutdown();
49
+ process.exit(0);
50
+ });
51
+ process.on('SIGTERM', async () => {
52
+ await bridge.shutdown();
53
+ process.exit(0);
54
+ });
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerPrompts(server: McpServer): void;