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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Claude Code와 Claude Desktop에서 한글(HWP) 문서를 AI로 자동 편집하는 MCP 서버입니다.
4
4
 
5
- 85이상의 도구로 문서 열기, 표 채우기, 텍스트 편집, 서식 설정, PDF 변환까지 모두 자동화할 수 있습니다.
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
- ## 기능 목록 (85개+)
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
- ### 이미지/레이아웃 (5개)
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
- ## v0.3.0 변경사항
524
-
525
- ### 버그 수정 (10건)
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
 
@@ -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', 'import win32com.client; o = win32com.client.gencache.EnsureDispatch("HWPFrame.HwpObject"); o.XHwpDocuments.Close(False); del o'], { timeout: 15000 });
295
- result.hwp = { found: true };
296
- }
297
- catch (err) {
298
- const msg = err.message || '';
299
- if (msg.includes('COM class not registered') || msg.includes('gencache')) {
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
- result.hwp = { found: true };
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: 'claude-code-hwp-mcp',
23
- version: '0.3.1',
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
- }, async ({ align, line_spacing, line_spacing_type, space_before, space_after, indent, left_margin, right_margin }) => {
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', '표에서 선택된 셀들을 병합합니다. 먼저 표에 진입한 상태여야 합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
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 r = await bridge.send('table_merge_cells', { table_index }, FILL_TIMEOUT);
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 배열 데이터로 새 표를 생성합니다. 현재 커서 위치에 표가 삽입됩니다. header_style=true로 행을 자동 스타일링(Bold+배경색)합니다.', {
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+연회색 배경, 기본 false)'),
540
- }, async ({ data, header_style }) => {
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.3.1",
4
- "description": "MCP server for HWP (한글) document automation via pyhwpx COM API. 85+ tools for document editing, analysis, and AI-powered filling.",
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": {