claude-code-hwp-mcp 0.3.1 → 0.5.1
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.
- package/README.md +66 -24
- package/dist/hwp-bridge.js +17 -12
- package/dist/index.js +2 -2
- package/dist/tools/analysis-tools.js +183 -0
- package/dist/tools/editing-tools.js +86 -7
- package/package.json +2 -2
- package/python/hwp_editor.py +276 -71
- package/python/hwp_service.py +429 -42
- package/python/ref_reader.py +167 -2
- package/python/requirements.txt +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Code와 Claude Desktop에서 한글(HWP) 문서를 AI로 자동 편집하는 MCP 서버입니다.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
94개 도구로 문서 열기, 표 채우기, 텍스트 편집, 서식 설정, 페이지 레이아웃, PDF 시각 검증까지 모두 자동화할 수 있습니다.
|
|
6
6
|
|
|
7
7
|
> Windows 전용 | 한글 2014 이상 | Python 3.8+ | Claude Code + Claude Desktop 지원
|
|
8
8
|
|
|
@@ -299,7 +299,7 @@ Python 경로, pyhwpx 설치 여부, 한글 프로그램 설치 및 실행 상
|
|
|
299
299
|
|
|
300
300
|
---
|
|
301
301
|
|
|
302
|
-
## 기능 목록 (
|
|
302
|
+
## 기능 목록 (94개)
|
|
303
303
|
|
|
304
304
|
### 환경/문서 관리 (6개)
|
|
305
305
|
|
|
@@ -379,16 +379,30 @@ Python 경로, pyhwpx 설치 여부, 한글 프로그램 설치 및 실행 상
|
|
|
379
379
|
| hwp_set_cell_color | 셀 배경색 설정 |
|
|
380
380
|
| hwp_set_table_border | 표 테두리 스타일 설정 |
|
|
381
381
|
|
|
382
|
-
###
|
|
382
|
+
### 페이지/레이아웃 (9개)
|
|
383
383
|
|
|
384
384
|
| 도구 | 설명 |
|
|
385
385
|
|------|------|
|
|
386
|
+
| hwp_set_page_setup | 여백, 용지 크기, 방향(가로/세로) 설정 |
|
|
387
|
+
| hwp_set_header_footer | 머리글/바닥글 삽입 |
|
|
388
|
+
| hwp_set_column | 다단 설정 (2단/3단, 구분선) |
|
|
389
|
+
| hwp_verify_layout | PDF→PNG 시각 검증 (PyMuPDF) |
|
|
386
390
|
| hwp_insert_picture | 이미지 삽입 |
|
|
387
391
|
| hwp_set_background_picture | 배경 이미지 설정 |
|
|
388
392
|
| hwp_insert_line | 선(줄) 삽입 |
|
|
389
393
|
| hwp_break_section | 섹션 나누기 |
|
|
390
394
|
| hwp_break_column | 다단 나누기 |
|
|
391
395
|
|
|
396
|
+
### 서식/그리기 (5개)
|
|
397
|
+
|
|
398
|
+
| 도구 | 설명 |
|
|
399
|
+
|------|------|
|
|
400
|
+
| hwp_apply_style | 문단 스타일 적용 ("제목1", "본문" 등) |
|
|
401
|
+
| hwp_set_cell_property | 셀 여백/수직정렬/텍스트방향/보호 |
|
|
402
|
+
| hwp_insert_textbox | 글상자 생성 (위치/크기 지정) |
|
|
403
|
+
| hwp_draw_line | 선 그리기 (두께/색상/스타일) |
|
|
404
|
+
| hwp_insert_caption | 표/그림 캡션 삽입 |
|
|
405
|
+
|
|
392
406
|
### 스마트/복합 도구 (16개)
|
|
393
407
|
|
|
394
408
|
| 도구 | 설명 |
|
|
@@ -520,38 +534,66 @@ HWP 바이너리 파일에서 `hwp_text_search`가 0건을 반환할 수 있습
|
|
|
520
534
|
|
|
521
535
|
---
|
|
522
536
|
|
|
523
|
-
##
|
|
524
|
-
|
|
525
|
-
###
|
|
526
|
-
|
|
537
|
+
## 변경 이력
|
|
538
|
+
|
|
539
|
+
### v0.5.0 (2026-03-25) — 94개 도구, 표 서식 대폭 강화
|
|
540
|
+
|
|
541
|
+
**신규 도구 9개:**
|
|
542
|
+
- `hwp_set_page_setup` — 여백, 용지 크기, 방향(가로/세로) 설정
|
|
543
|
+
- `hwp_set_header_footer` — 머리글/바닥글 삽입 (CreateAction 방식, 대화상자 없음)
|
|
544
|
+
- `hwp_set_column` — 다단 설정 (2단/3단, 구분선)
|
|
545
|
+
- `hwp_verify_layout` — PDF→PNG 시각 검증 (PyMuPDF 필요)
|
|
546
|
+
- `hwp_apply_style` — 문단 스타일 적용 ("제목1", "본문" 등)
|
|
547
|
+
- `hwp_set_cell_property` — 셀 여백/수직정렬/텍스트방향/보호
|
|
548
|
+
- `hwp_insert_textbox` — 글상자 생성 (위치/크기 지정)
|
|
549
|
+
- `hwp_draw_line` — 선 그리기 (두께/색상/스타일)
|
|
550
|
+
- `hwp_insert_caption` — 표/그림 캡션 삽입
|
|
551
|
+
|
|
552
|
+
**텍스트 서식 속성 25+ 추가 (insert_text):**
|
|
553
|
+
- 밑줄: underline_type(7종), underline_color
|
|
554
|
+
- 취소선: strikeout_type(4종), strikeout_color
|
|
555
|
+
- 효과: superscript, subscript, outline, shadow, emboss, engrave, small_caps
|
|
556
|
+
- 글꼴: font_name_latin(라틴 전용), shadow_color, use_kerning
|
|
557
|
+
- 배경: bg_color
|
|
558
|
+
|
|
559
|
+
**문단 서식 속성 11개 추가 (set_paragraph_style):**
|
|
560
|
+
- page_break_before, keep_with_next, widow_orphan
|
|
561
|
+
- line_wrap, snap_to_grid, auto_space_eAsian_eng/num
|
|
562
|
+
- break_latin_word, heading_type, keep_lines_together, condense
|
|
563
|
+
|
|
564
|
+
**표 기능 핵심 개선:**
|
|
565
|
+
- 셀 배경색: pyhwpx `cell_fill()` 내장 메서드 사용 (안정적)
|
|
566
|
+
- 셀 텍스트 정렬: `TableCellAlignCenterCenter` 액션 사용 (삽입 후 적용)
|
|
567
|
+
- 셀 병합: `TableCellBlockExtend` 방식으로 정확한 블록 선택
|
|
568
|
+
- 표 생성: col_widths(mm), row_heights(mm), alignment(left/center/right), header_style(Bold)
|
|
569
|
+
- 테두리: color(#RRGGBB), edges(방향별 적용) 지원
|
|
570
|
+
|
|
571
|
+
**버그 수정:**
|
|
572
|
+
- insert_text 자동 줄바꿈 — 각 호출이 독립 문단으로 생성
|
|
573
|
+
- set_header_footer: `CreateAction("HeaderFooter")` 방식 (대화상자 타임아웃 해결)
|
|
574
|
+
- apply_style: `Execute("Style")` + `SetItem("StyleName")` (대화상자 방지)
|
|
575
|
+
- draw_line: 대화형 InsertLine fallback 제거
|
|
576
|
+
- insert_footnote/endnote: try/except + CloseEx 추가
|
|
577
|
+
- get_cursor_context: page → total_pages + current_page(KeyIndicator)
|
|
578
|
+
|
|
579
|
+
### v0.3.0 — HWPX XML 라우팅 + 10건 버그 수정
|
|
580
|
+
|
|
581
|
+
**버그 수정 (10건):**
|
|
527
582
|
- SelectAll 문서 파괴 수정 (table_create_from_data, gantt_chart 등)
|
|
528
583
|
- find_replace 전후 텍스트 비교 검증으로 개선
|
|
529
584
|
- text_search 선택 영역 기반 판단으로 개선
|
|
530
585
|
- find_and_append 커서 유실 수정 (Cancel → MoveRight)
|
|
531
586
|
- 버퍼 오버플로우 시 개별 요청만 거부
|
|
532
587
|
- XHwpMessageBoxMode 복원 (close_document)
|
|
533
|
-
- insert_markdown 표 파싱 추가
|
|
534
588
|
- blank_template.hwpx 프로그래밍적 생성 (파일 의존성 제거)
|
|
535
|
-
- except pass → stderr 로깅 변환
|
|
536
|
-
- MAX_TABLES scanned_tables 필드 분리
|
|
537
|
-
|
|
538
|
-
### HWPX XML 라우팅
|
|
539
|
-
|
|
540
|
-
HWPX 파일의 텍스트 검색/치환을 Node.js XML 엔진으로 직접 처리:
|
|
541
|
-
- hwp_find_replace → XML replaceTextInSection
|
|
542
|
-
- hwp_find_replace_multi → XML 루프 치환
|
|
543
|
-
- hwp_find_replace_nth → XML N번째 치환
|
|
544
|
-
- hwp_find_and_append → XML findAndAppend
|
|
545
|
-
- hwp_text_search → XML searchTextInSection
|
|
546
|
-
|
|
547
|
-
XML 실패 시(파일 잠금 등) COM 경로로 자동 폴백.
|
|
548
589
|
|
|
549
|
-
|
|
590
|
+
**HWPX XML 라우팅:**
|
|
591
|
+
- HWPX 파일의 텍스트 검색/치환을 Node.js XML 엔진으로 직접 처리
|
|
592
|
+
- XML 실패 시(파일 잠금 등) COM 경로로 자동 폴백
|
|
550
593
|
|
|
594
|
+
**환경 진단 개선:**
|
|
551
595
|
- Microsoft Store Python 자동 감지 + 경고
|
|
552
596
|
- 한글 프로세스 실행 여부 체크 (tasklist)
|
|
553
|
-
- Python 실행 경로 표시
|
|
554
|
-
- postinstall 스크립트 ESM 호환
|
|
555
597
|
|
|
556
598
|
---
|
|
557
599
|
|
package/dist/hwp-bridge.js
CHANGED
|
@@ -289,23 +289,28 @@ export class HwpBridge {
|
|
|
289
289
|
};
|
|
290
290
|
return result;
|
|
291
291
|
}
|
|
292
|
-
// 3) 한글(HWP) COM
|
|
292
|
+
// 3) 한글(HWP) 설치 체크 — COM Dispatch 대신 파일 존재 + pyhwpx import 확인
|
|
293
|
+
// (COM Dispatch는 빈 한글 문서를 열어버리는 부작용이 있으므로 제거)
|
|
293
294
|
try {
|
|
294
|
-
await execFileAsync(pythonExe, ['-c',
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
result.hwp = {
|
|
301
|
-
found: false,
|
|
302
|
-
guide: '한글(HWP) 프로그램이 설치되지 않았습니다.\n→ 한컴오피스 한글 설치 필요 (한글 2014 이상)\n→ 설치 후 한글을 한번 실행하여 초기 설정 완료',
|
|
303
|
-
};
|
|
295
|
+
const { stdout } = await execFileAsync(pythonExe, ['-c',
|
|
296
|
+
'import os; paths = [r"C:\\Program Files\\Hancom", r"C:\\Program Files (x86)\\Hancom"]; ' +
|
|
297
|
+
'found = any(os.path.isdir(p) for p in paths); ' +
|
|
298
|
+
'print("installed" if found else "not_found")'
|
|
299
|
+
], { timeout: 5000 });
|
|
300
|
+
if (stdout.trim() === 'installed') {
|
|
301
|
+
result.hwp = { found: true };
|
|
304
302
|
}
|
|
305
303
|
else {
|
|
306
|
-
|
|
304
|
+
// 폴더가 없어도 pyhwpx가 COM을 찾을 수 있으므로 pyhwpx import 성공이면 OK
|
|
305
|
+
result.hwp = { found: true, guide: '한컴 설치 폴더를 찾을 수 없지만, pyhwpx가 설치되어 있으므로 동작할 수 있습니다.' };
|
|
307
306
|
}
|
|
308
307
|
}
|
|
308
|
+
catch {
|
|
309
|
+
result.hwp = {
|
|
310
|
+
found: false,
|
|
311
|
+
guide: '한글(HWP) 프로그램이 설치되지 않았습니다.\n→ 한컴오피스 한글 설치 필요 (한글 2014 이상)\n→ 설치 후 한글을 한번 실행하여 초기 설정 완료',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
309
314
|
// 4) 한글 프로세스 실행 여부 체크
|
|
310
315
|
try {
|
|
311
316
|
const { stdout } = await execFileAsync('tasklist', ['/FI', 'IMAGENAME eq Hwp.exe', '/NH'], { timeout: 5000 });
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ const resolvedToolset = validToolsets.includes(toolset)
|
|
|
19
19
|
: 'standard';
|
|
20
20
|
const bridge = new HwpBridge();
|
|
21
21
|
const server = new McpServer({
|
|
22
|
-
name: '
|
|
23
|
-
version: '0.
|
|
22
|
+
name: 'hwp-studio',
|
|
23
|
+
version: '0.4.1',
|
|
24
24
|
});
|
|
25
25
|
setupServer(server, bridge, resolvedToolset);
|
|
26
26
|
const transport = new StdioServerTransport();
|
|
@@ -428,5 +428,188 @@ export function registerAnalysisTools(server, bridge, toolset = 'standard') {
|
|
|
428
428
|
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
429
429
|
}
|
|
430
430
|
});
|
|
431
|
+
// ── 시각적 레이아웃 검증 ──
|
|
432
|
+
server.tool('hwp_verify_layout', '현재 문서를 PNG 이미지로 변환하여 경로를 반환합니다. Claude가 Read 도구로 이미지를 읽어 표 구조, 셀 병합, 열 너비, 정렬 등을 시각적으로 검증합니다. 공문서 생성 후 결과물 확인에 사용하세요. PyMuPDF 필요(pip install PyMuPDF).', {
|
|
433
|
+
pages: z.string().optional().describe('확인할 페이지 (예: "1", "5-7". 생략 시 전체)'),
|
|
434
|
+
}, async ({ pages }) => {
|
|
435
|
+
if (!bridge.getCurrentDocument())
|
|
436
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
437
|
+
try {
|
|
438
|
+
await bridge.ensureRunning();
|
|
439
|
+
const params = {};
|
|
440
|
+
if (pages)
|
|
441
|
+
params.pages = pages;
|
|
442
|
+
const r = await bridge.send('verify_layout', params, 60000);
|
|
443
|
+
if (!r.success)
|
|
444
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
445
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
// ── 페이지 설정 ──
|
|
452
|
+
server.tool('hwp_set_page_setup', '페이지 여백, 용지 크기, 방향을 설정합니다. 공문서 작성 전 페이지 설정에 사용하세요.', {
|
|
453
|
+
top_margin: z.number().optional().describe('위쪽 여백 (mm)'),
|
|
454
|
+
bottom_margin: z.number().optional().describe('아래쪽 여백 (mm)'),
|
|
455
|
+
left_margin: z.number().optional().describe('왼쪽 여백 (mm)'),
|
|
456
|
+
right_margin: z.number().optional().describe('오른쪽 여백 (mm)'),
|
|
457
|
+
header_margin: z.number().optional().describe('머리말 여백 (mm)'),
|
|
458
|
+
footer_margin: z.number().optional().describe('꼬리말 여백 (mm)'),
|
|
459
|
+
orientation: z.enum(['portrait', 'landscape']).optional().describe('용지 방향'),
|
|
460
|
+
paper_width: z.number().optional().describe('용지 너비 (mm, 기본 A4=210)'),
|
|
461
|
+
paper_height: z.number().optional().describe('용지 높이 (mm, 기본 A4=297)'),
|
|
462
|
+
}, async (params) => {
|
|
463
|
+
if (!bridge.getCurrentDocument())
|
|
464
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
465
|
+
try {
|
|
466
|
+
await bridge.ensureRunning();
|
|
467
|
+
const r = await bridge.send('set_page_setup', params);
|
|
468
|
+
if (!r.success)
|
|
469
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
470
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
// ── 셀 속성 설정 ──
|
|
477
|
+
server.tool('hwp_set_cell_property', '표 셀의 여백, 수직 정렬, 텍스트 방향, 보호 등 속성을 설정합니다.', {
|
|
478
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
479
|
+
tab: z.number().int().min(0).describe('셀 탭 인덱스'),
|
|
480
|
+
vert_align: z.enum(['top', 'middle', 'bottom']).optional().describe('수직 정렬'),
|
|
481
|
+
margin_left: z.number().optional().describe('셀 왼쪽 여백 (mm)'),
|
|
482
|
+
margin_right: z.number().optional().describe('셀 오른쪽 여백 (mm)'),
|
|
483
|
+
margin_top: z.number().optional().describe('셀 위쪽 여백 (mm)'),
|
|
484
|
+
margin_bottom: z.number().optional().describe('셀 아래쪽 여백 (mm)'),
|
|
485
|
+
text_direction: z.number().int().min(0).max(1).optional().describe('텍스트 방향 (0=가로, 1=세로)'),
|
|
486
|
+
protected: z.boolean().optional().describe('셀 보호'),
|
|
487
|
+
}, async (params) => {
|
|
488
|
+
if (!bridge.getCurrentDocument())
|
|
489
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
490
|
+
try {
|
|
491
|
+
await bridge.ensureRunning();
|
|
492
|
+
const r = await bridge.send('set_cell_property', params);
|
|
493
|
+
if (!r.success)
|
|
494
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
495
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
// ── 글상자 생성 ──
|
|
502
|
+
server.tool('hwp_insert_textbox', '글상자(텍스트박스)를 생성합니다. x/y로 위치, width/height로 크기를 지정합니다. 결재란 등 위치 지정이 필요한 요소에 사용하세요.', {
|
|
503
|
+
x: z.number().optional().describe('X 위치 (mm, 페이지 기준, 기본 0)'),
|
|
504
|
+
y: z.number().optional().describe('Y 위치 (mm, 페이지 기준, 기본 0)'),
|
|
505
|
+
width: z.number().optional().describe('너비 (mm, 기본 60)'),
|
|
506
|
+
height: z.number().optional().describe('높이 (mm, 기본 30)'),
|
|
507
|
+
text: z.string().optional().describe('글상자 내 텍스트'),
|
|
508
|
+
border: z.boolean().optional().describe('테두리 표시 (기본 true)'),
|
|
509
|
+
}, async (params) => {
|
|
510
|
+
if (!bridge.getCurrentDocument())
|
|
511
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
512
|
+
try {
|
|
513
|
+
await bridge.ensureRunning();
|
|
514
|
+
const r = await bridge.send('insert_textbox', params);
|
|
515
|
+
if (!r.success)
|
|
516
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
517
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
// ── 선 그리기 (강화) ──
|
|
524
|
+
server.tool('hwp_draw_line', '선을 그립니다. 두께, 색상, 스타일을 지정할 수 있습니다. hwp_insert_line보다 상세한 제어가 가능합니다.', {
|
|
525
|
+
width: z.number().optional().describe('선 두께'),
|
|
526
|
+
color: z.string().optional().describe('선 색상 (#RRGGBB 또는 [R,G,B])'),
|
|
527
|
+
style: z.number().int().min(0).max(5).optional().describe('선 스타일 (0=실선, 1=파선, 2=점선, 3=1점쇄선, 4=2점쇄선)'),
|
|
528
|
+
}, async (params) => {
|
|
529
|
+
if (!bridge.getCurrentDocument())
|
|
530
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
531
|
+
try {
|
|
532
|
+
await bridge.ensureRunning();
|
|
533
|
+
const r = await bridge.send('draw_line', params);
|
|
534
|
+
if (!r.success)
|
|
535
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
536
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
// ── 머리글/바닥글 ──
|
|
543
|
+
server.tool('hwp_set_header_footer', '머리글 또는 바닥글을 설정합니다. 기관명, 페이지번호 등을 삽입할 때 사용하세요.', {
|
|
544
|
+
type: z.enum(['header', 'footer']).describe('머리글 또는 바닥글'),
|
|
545
|
+
text: z.string().optional().describe('삽입할 텍스트'),
|
|
546
|
+
}, async ({ type, text }) => {
|
|
547
|
+
if (!bridge.getCurrentDocument())
|
|
548
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
549
|
+
try {
|
|
550
|
+
await bridge.ensureRunning();
|
|
551
|
+
const r = await bridge.send('set_header_footer', { type, text });
|
|
552
|
+
if (!r.success)
|
|
553
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
554
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
// ── 스타일 적용 ──
|
|
561
|
+
server.tool('hwp_apply_style', '현재 커서 위치에 문단 스타일을 적용합니다. "제목1", "본문", "개요1" 등 한글에 정의된 스타일을 사용합니다.', {
|
|
562
|
+
style_name: z.string().describe('스타일 이름 (예: "제목1", "본문", "개요 1")'),
|
|
563
|
+
}, async ({ style_name }) => {
|
|
564
|
+
if (!bridge.getCurrentDocument())
|
|
565
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
566
|
+
try {
|
|
567
|
+
await bridge.ensureRunning();
|
|
568
|
+
const r = await bridge.send('apply_style', { style_name });
|
|
569
|
+
if (!r.success)
|
|
570
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
571
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
// ── 다단 설정 ──
|
|
578
|
+
server.tool('hwp_set_column', '현재 섹션의 다단을 설정합니다. 2단/3단 레이아웃에 사용하세요.', {
|
|
579
|
+
count: z.number().int().min(1).max(10).describe('단 수 (기본 2)'),
|
|
580
|
+
gap: z.number().optional().describe('단 간격 (mm, 기본 10)'),
|
|
581
|
+
line_type: z.number().int().min(0).max(5).optional().describe('구분선 종류 (0=없음, 1=실선)'),
|
|
582
|
+
}, async ({ count, gap, line_type }) => {
|
|
583
|
+
if (!bridge.getCurrentDocument())
|
|
584
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
585
|
+
try {
|
|
586
|
+
await bridge.ensureRunning();
|
|
587
|
+
const r = await bridge.send('set_column', { count, gap, line_type });
|
|
588
|
+
if (!r.success)
|
|
589
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
590
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
// ── 캡션 삽입 ──
|
|
597
|
+
server.tool('hwp_insert_caption', '표나 그림에 캡션(제목)을 삽입합니다.', {
|
|
598
|
+
text: z.string().optional().describe('캡션 텍스트'),
|
|
599
|
+
side: z.number().int().min(0).max(3).optional().describe('캡션 위치 (0=왼쪽, 1=오른쪽, 2=위, 3=아래)'),
|
|
600
|
+
}, async ({ text, side }) => {
|
|
601
|
+
if (!bridge.getCurrentDocument())
|
|
602
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
603
|
+
try {
|
|
604
|
+
await bridge.ensureRunning();
|
|
605
|
+
const r = await bridge.send('insert_caption', { text, side });
|
|
606
|
+
if (!r.success)
|
|
607
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
608
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
612
|
+
}
|
|
613
|
+
});
|
|
431
614
|
} // end toolset !== 'minimal'
|
|
432
615
|
}
|
|
@@ -53,7 +53,9 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
53
53
|
font_name: z.string().optional().describe('글꼴 이름'),
|
|
54
54
|
char_spacing: z.number().optional().describe('자간 (%)'),
|
|
55
55
|
width_ratio: z.number().optional().describe('장평 (%)'),
|
|
56
|
+
align: z.enum(['left', 'center', 'right', 'justify']).optional().describe('셀 텍스트 정렬'),
|
|
56
57
|
}).optional().describe('셀별 서식 (생략 시 기존 셀 서식 상속)'),
|
|
58
|
+
vert_align: z.enum(['top', 'middle', 'bottom']).optional().describe('셀 수직 정렬'),
|
|
57
59
|
})).describe('채울 셀 목록. label, tab, row+col 중 하나 필수'),
|
|
58
60
|
})).describe('채울 표 배열'),
|
|
59
61
|
}, async ({ file_path, tables }) => {
|
|
@@ -81,7 +83,7 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
81
83
|
if (tabCells.length > 0) {
|
|
82
84
|
const resp = await bridge.send('fill_by_tab', {
|
|
83
85
|
table_index: table.index,
|
|
84
|
-
cells: tabCells.map(c => ({ tab: c.tab, text: c.text, ...(c.style ? { style: c.style } : {}) })),
|
|
86
|
+
cells: tabCells.map(c => ({ tab: c.tab, text: c.text, ...(c.style ? { style: c.style } : {}), ...(c.vert_align ? { vert_align: c.vert_align } : {}) })),
|
|
85
87
|
}, FILL_TIMEOUT);
|
|
86
88
|
if (!resp.success) {
|
|
87
89
|
return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
|
|
@@ -280,6 +282,24 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
280
282
|
width_ratio: z.number().optional().describe('장평 (%, 기본 100. 100 미만=좁게, 100 초과=넓게)'),
|
|
281
283
|
font_name_hanja: z.string().optional().describe('한자 글꼴 이름'),
|
|
282
284
|
font_name_japanese: z.string().optional().describe('일본어 글꼴 이름'),
|
|
285
|
+
font_name_latin: z.string().optional().describe('라틴(영문) 전용 글꼴'),
|
|
286
|
+
underline_type: z.number().int().min(0).max(7).optional().describe('밑줄 종류 (0=없음,1=실선,2=이중,3=점선,4=파선,5=1점쇄선,6=물결,7=굵은실선)'),
|
|
287
|
+
underline_color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('밑줄 색상 [R,G,B]'),
|
|
288
|
+
strikeout_type: z.number().int().min(0).max(3).optional().describe('취소선 종류 (0=없음,1=단일,2=이중,3=굵은)'),
|
|
289
|
+
strikeout_color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('취소선 색상 [R,G,B]'),
|
|
290
|
+
superscript: z.boolean().optional().describe('위 첨자'),
|
|
291
|
+
subscript: z.boolean().optional().describe('아래 첨자'),
|
|
292
|
+
outline: z.boolean().optional().describe('외곽선'),
|
|
293
|
+
shadow: z.boolean().optional().describe('그림자'),
|
|
294
|
+
shadow_color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('그림자 색상 [R,G,B]'),
|
|
295
|
+
shadow_offset_x: z.number().optional().describe('그림자 X 오프셋'),
|
|
296
|
+
shadow_offset_y: z.number().optional().describe('그림자 Y 오프셋'),
|
|
297
|
+
emboss: z.boolean().optional().describe('양각'),
|
|
298
|
+
engrave: z.boolean().optional().describe('음각'),
|
|
299
|
+
small_caps: z.boolean().optional().describe('작은 대문자'),
|
|
300
|
+
underline_shape: z.number().int().optional().describe('밑줄 모양'),
|
|
301
|
+
strikeout_shape: z.number().int().optional().describe('취소선 모양'),
|
|
302
|
+
use_kerning: z.boolean().optional().describe('커닝 (자동 자간 조정)'),
|
|
283
303
|
}).optional().describe('텍스트 서식 옵션'),
|
|
284
304
|
}, async ({ text, color, style }) => {
|
|
285
305
|
if (!bridge.getCurrentDocument()) {
|
|
@@ -317,7 +337,18 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
317
337
|
indent: z.number().optional().describe('첫 줄 들여쓰기 (pt, 양수=들여쓰기, 음수=내어쓰기)'),
|
|
318
338
|
left_margin: z.number().optional().describe('왼쪽 여백/나머지 줄 시작위치 (pt)'),
|
|
319
339
|
right_margin: z.number().optional().describe('오른쪽 여백 (pt)'),
|
|
320
|
-
|
|
340
|
+
page_break_before: z.boolean().optional().describe('문단 앞 페이지 나누기'),
|
|
341
|
+
keep_with_next: z.boolean().optional().describe('다음 문단과 함께 (제목+본문 분리 방지)'),
|
|
342
|
+
widow_orphan: z.boolean().optional().describe('과부/고아 방지'),
|
|
343
|
+
line_wrap: z.number().int().optional().describe('줄 바꿈 방식'),
|
|
344
|
+
snap_to_grid: z.boolean().optional().describe('그리드에 맞춤'),
|
|
345
|
+
auto_space_eAsian_eng: z.boolean().optional().describe('한영 자동 간격'),
|
|
346
|
+
auto_space_eAsian_num: z.boolean().optional().describe('한숫자 자동 간격'),
|
|
347
|
+
break_latin_word: z.number().int().optional().describe('영문 줄바꿈 (0=단어, 1=글자)'),
|
|
348
|
+
heading_type: z.number().int().optional().describe('제목 수준 (개요)'),
|
|
349
|
+
keep_lines_together: z.boolean().optional().describe('줄 함께 유지 (문단 분리 방지)'),
|
|
350
|
+
condense: z.number().int().optional().describe('문단 압축'),
|
|
351
|
+
}, async ({ align, line_spacing, line_spacing_type, space_before, space_after, indent, left_margin, right_margin, page_break_before, keep_with_next, widow_orphan, line_wrap, snap_to_grid, auto_space_eAsian_eng, auto_space_eAsian_num, break_latin_word, heading_type, keep_lines_together, condense }) => {
|
|
321
352
|
if (!bridge.getCurrentDocument()) {
|
|
322
353
|
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
323
354
|
}
|
|
@@ -340,6 +371,28 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
340
371
|
style.left_margin = left_margin;
|
|
341
372
|
if (right_margin !== undefined)
|
|
342
373
|
style.right_margin = right_margin;
|
|
374
|
+
if (page_break_before !== undefined)
|
|
375
|
+
style.page_break_before = page_break_before;
|
|
376
|
+
if (keep_with_next !== undefined)
|
|
377
|
+
style.keep_with_next = keep_with_next;
|
|
378
|
+
if (widow_orphan !== undefined)
|
|
379
|
+
style.widow_orphan = widow_orphan;
|
|
380
|
+
if (line_wrap !== undefined)
|
|
381
|
+
style.line_wrap = line_wrap;
|
|
382
|
+
if (snap_to_grid !== undefined)
|
|
383
|
+
style.snap_to_grid = snap_to_grid;
|
|
384
|
+
if (auto_space_eAsian_eng !== undefined)
|
|
385
|
+
style.auto_space_eAsian_eng = auto_space_eAsian_eng;
|
|
386
|
+
if (auto_space_eAsian_num !== undefined)
|
|
387
|
+
style.auto_space_eAsian_num = auto_space_eAsian_num;
|
|
388
|
+
if (break_latin_word !== undefined)
|
|
389
|
+
style.break_latin_word = break_latin_word;
|
|
390
|
+
if (heading_type !== undefined)
|
|
391
|
+
style.heading_type = heading_type;
|
|
392
|
+
if (keep_lines_together !== undefined)
|
|
393
|
+
style.keep_lines_together = keep_lines_together;
|
|
394
|
+
if (condense !== undefined)
|
|
395
|
+
style.condense = condense;
|
|
343
396
|
const response = await bridge.send('set_paragraph_style', { style });
|
|
344
397
|
if (!response.success) {
|
|
345
398
|
return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
|
|
@@ -519,12 +572,27 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
519
572
|
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
520
573
|
}
|
|
521
574
|
});
|
|
522
|
-
server.tool('hwp_table_merge_cells', '
|
|
575
|
+
server.tool('hwp_table_merge_cells', '표의 셀을 병합합니다. start_row/col ~ end_row/col로 범위를 지정하면 해당 영역이 병합됩니다. 범위 미지정 시 현재 선택된 셀이 병합됩니다. 병합 순서는 하단→상단이 안전합니다.', {
|
|
576
|
+
table_index: z.number().int().min(0).describe('표 인덱스'),
|
|
577
|
+
start_row: z.number().int().min(0).optional().describe('시작 행 (0부터)'),
|
|
578
|
+
start_col: z.number().int().min(0).optional().describe('시작 열 (0부터)'),
|
|
579
|
+
end_row: z.number().int().min(0).optional().describe('끝 행 (0부터)'),
|
|
580
|
+
end_col: z.number().int().min(0).optional().describe('끝 열 (0부터)'),
|
|
581
|
+
}, async ({ table_index, start_row, start_col, end_row, end_col }) => {
|
|
523
582
|
if (!bridge.getCurrentDocument())
|
|
524
583
|
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
525
584
|
try {
|
|
526
585
|
await bridge.ensureRunning();
|
|
527
|
-
const
|
|
586
|
+
const params = { table_index };
|
|
587
|
+
if (start_row !== undefined)
|
|
588
|
+
params.start_row = start_row;
|
|
589
|
+
if (start_col !== undefined)
|
|
590
|
+
params.start_col = start_col;
|
|
591
|
+
if (end_row !== undefined)
|
|
592
|
+
params.end_row = end_row;
|
|
593
|
+
if (end_col !== undefined)
|
|
594
|
+
params.end_col = end_col;
|
|
595
|
+
const r = await bridge.send('table_merge_cells', params, FILL_TIMEOUT);
|
|
528
596
|
if (!r.success)
|
|
529
597
|
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
530
598
|
bridge.setCachedAnalysis(null);
|
|
@@ -534,10 +602,13 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
534
602
|
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
|
|
535
603
|
}
|
|
536
604
|
});
|
|
537
|
-
server.tool('hwp_table_create_from_data', '2D 배열 데이터로 새 표를 생성합니다.
|
|
605
|
+
server.tool('hwp_table_create_from_data', '2D 배열 데이터로 새 표를 생성합니다. col_widths로 열 너비(mm), row_heights로 행 높이(mm)를 지정할 수 있습니다. 공문서 표 등 정밀한 레이아웃에 사용하세요.', {
|
|
538
606
|
data: z.array(z.array(z.string())).describe('2D 배열 데이터 [["헤더1","헤더2"],["값1","값2"]]'),
|
|
539
|
-
header_style: z.boolean().optional().describe('첫 행을 헤더로 자동 스타일링 (Bold
|
|
540
|
-
|
|
607
|
+
header_style: z.boolean().optional().describe('첫 행을 헤더로 자동 스타일링 (Bold+배경색)'),
|
|
608
|
+
col_widths: z.array(z.number()).optional().describe('열 너비 배열 (mm 단위, 예: [18, 65, 23, 23])'),
|
|
609
|
+
row_heights: z.array(z.number()).optional().describe('행 높이 배열 (mm 단위, 예: [10, 12, 12])'),
|
|
610
|
+
alignment: z.enum(['left', 'center', 'right']).optional().describe('표 정렬 (기본: left)'),
|
|
611
|
+
}, async ({ data, header_style, col_widths, row_heights, alignment }) => {
|
|
541
612
|
if (!bridge.getCurrentDocument())
|
|
542
613
|
return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
|
|
543
614
|
try {
|
|
@@ -545,6 +616,12 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
545
616
|
const params = { data };
|
|
546
617
|
if (header_style)
|
|
547
618
|
params.header_style = header_style;
|
|
619
|
+
if (col_widths)
|
|
620
|
+
params.col_widths = col_widths;
|
|
621
|
+
if (row_heights)
|
|
622
|
+
params.row_heights = row_heights;
|
|
623
|
+
if (alignment)
|
|
624
|
+
params.alignment = alignment;
|
|
548
625
|
const r = await bridge.send('table_create_from_data', params, FILL_TIMEOUT);
|
|
549
626
|
if (!r.success)
|
|
550
627
|
return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
|
|
@@ -974,6 +1051,8 @@ export function registerEditingTools(server, bridge, toolset = 'standard') {
|
|
|
974
1051
|
style: z.object({
|
|
975
1052
|
line_type: z.number().int().min(0).max(5).optional().describe('선 종류: 0=없음, 1=실선, 2=파선, 3=점선, 4=1점쇄선, 5=2점쇄선'),
|
|
976
1053
|
line_width: z.number().optional().describe('선 두께 (pt 단위)'),
|
|
1054
|
+
color: z.string().optional().describe('테두리 색상 (#RRGGBB, 예: "#003366")'),
|
|
1055
|
+
edges: z.array(z.enum(['left', 'right', 'top', 'bottom'])).optional().describe('적용할 방향 (생략 시 전체)'),
|
|
977
1056
|
}).optional().describe('테두리 스타일'),
|
|
978
1057
|
}, async ({ table_index, cells, style }) => {
|
|
979
1058
|
if (!bridge.getCurrentDocument())
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hwp-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for HWP (한글) document automation via pyhwpx COM API.
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "MCP server for HWP (한글) document automation via pyhwpx COM API. 94 tools for document editing, analysis, table formatting, and AI-powered filling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|