HWP MCP Server
by jkf87
Verified
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import json
import traceback
import logging
import ssl
from threading import Thread
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
filename="hwp_mcp_stdio_server.log",
filemode="a",
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# 추가 스트림 핸들러 설정 (별도로 추가)
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger = logging.getLogger("hwp-mcp-stdio-server")
logger.addHandler(stderr_handler)
# Optional: Disable SSL certificate validation for development
ssl._create_default_https_context = ssl._create_unverified_context
# Set up paths
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
try:
# Import FastMCP library
from mcp.server.fastmcp import FastMCP
logger.info("FastMCP successfully imported")
except ImportError as e:
logger.error(f"Failed to import FastMCP: {str(e)}")
print(f"Error: Failed to import FastMCP. Please install with 'pip install mcp'", file=sys.stderr)
sys.exit(1)
# Try to import HwpController
try:
from src.tools.hwp_controller import HwpController
logger.info("HwpController imported successfully")
except ImportError as e:
logger.error(f"Failed to import HwpController: {str(e)}")
# Try alternate paths
try:
sys.path.append(os.path.join(current_dir, "src"))
sys.path.append(os.path.join(current_dir, "src", "tools"))
from hwp_controller import HwpController
logger.info("HwpController imported from alternate path")
except ImportError as e2:
logger.error(f"Could not find HwpController in any path: {str(e2)}")
print(f"Error: Could not find HwpController module", file=sys.stderr)
sys.exit(1)
# Try to import HwpTableTools
try:
from src.tools.hwp_table_tools import HwpTableTools
logger.info("HwpTableTools imported successfully")
except ImportError as e:
logger.error(f"Failed to import HwpTableTools: {str(e)}")
# Try alternate paths
try:
from hwp_table_tools import HwpTableTools
logger.info("HwpTableTools imported from alternate path")
except ImportError as e2:
logger.error(f"Could not find HwpTableTools in any path: {str(e2)}")
print(f"Error: Could not find HwpTableTools module", file=sys.stderr)
sys.exit(1)
# Initialize FastMCP server
mcp = FastMCP(
"hwp-mcp",
version="0.1.0",
description="HWP MCP Server for controlling Hangul Word Processor",
dependencies=["pywin32>=305"],
env_vars={}
)
# Global HWP controller instance
hwp_controller = None
# Global HWP table tools instance
hwp_table_tools = None
def get_hwp_controller():
"""Get or create HwpController instance."""
global hwp_controller, hwp_table_tools
if hwp_controller is None:
logger.info("Creating HwpController instance...")
try:
hwp_controller = HwpController()
if not hwp_controller.connect(visible=True):
logger.error("Failed to connect to HWP program")
return None
# 테이블 도구 인스턴스도 초기화
hwp_table_tools = HwpTableTools(hwp_controller)
logger.info("Successfully connected to HWP program")
except Exception as e:
logger.error(f"Error creating HwpController: {str(e)}", exc_info=True)
return None
return hwp_controller
def get_hwp_table_tools():
"""Get or create HwpTableTools instance."""
global hwp_table_tools, hwp_controller
if hwp_table_tools is None:
hwp_controller = get_hwp_controller()
if hwp_controller:
hwp_table_tools = HwpTableTools(hwp_controller)
return hwp_table_tools
@mcp.tool()
def hwp_create() -> str:
"""Create a new HWP document."""
try:
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
if hwp.create_new_document():
logger.info("Successfully created new document")
return "New document created successfully"
else:
return "Error: Failed to create new document"
except Exception as e:
logger.error(f"Error creating document: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_open(path: str) -> str:
"""Open an existing HWP document."""
try:
if not path:
return "Error: File path is required"
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
if hwp.open_document(path):
logger.info(f"Successfully opened document: {path}")
return f"Document opened: {path}"
else:
return "Error: Failed to open document"
except Exception as e:
logger.error(f"Error opening document: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_save(path: str = None) -> str:
"""Save the current HWP document."""
try:
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
if path:
if hwp.save_document(path):
logger.info(f"Successfully saved document to: {path}")
return f"Document saved to: {path}"
else:
return "Error: Failed to save document"
else:
temp_path = os.path.join(os.getcwd(), "temp_document.hwp")
if hwp.save_document(temp_path):
logger.info(f"Successfully saved document to temporary location: {temp_path}")
return f"Document saved to: {temp_path}"
else:
return "Error: Failed to save document"
except Exception as e:
logger.error(f"Error saving document: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_insert_text(text: str, preserve_linebreaks: bool = True) -> str:
"""Insert text at the current cursor position."""
try:
if not text:
return "Error: Text is required"
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
# 현재 커서가 표 안에 있는지 확인
is_in_table = False
try:
hwp.hwp.Run("TableCellBlock")
hwp.hwp.Run("Cancel")
is_in_table = True
except:
is_in_table = False
# 줄바꿈 문자 처리
if preserve_linebreaks and ('\n' in text or '\\n' in text):
# 이스케이프된 줄바꿈 문자(\n)와 실제 줄바꿈 문자 모두 처리
processed_text = text.replace('\\n', '\n')
lines = processed_text.split('\n')
success = True
for i, line in enumerate(lines):
if not hwp.insert_text(line):
success = False
break
# 마지막 줄이 아니면 줄바꿈 삽입
if i < len(lines) - 1:
hwp.insert_paragraph()
if success:
logger.info("Successfully inserted text with line breaks")
return "Text with line breaks inserted successfully"
else:
return "Error: Failed to insert text with line breaks"
else:
if hwp.insert_text(text):
# 표 안이 아닐 경우에만 커서를 오른쪽으로 이동
if not is_in_table:
# 현재 위치 저장
current_pos = hwp.hwp.GetPos()
if current_pos:
# 텍스트 길이만큼 오른쪽으로 이동
for _ in range(len(text)):
hwp.hwp.Run("CharRight")
logger.info("Successfully inserted text")
return "Text inserted successfully"
else:
return "Error: Failed to insert text"
except Exception as e:
logger.error(f"Error inserting text: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_set_font(
name: str = None,
size: int = None,
bold: bool = False,
italic: bool = False,
underline: bool = False,
select_previous_text: bool = False
) -> str:
"""Set font properties for selected text."""
try:
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
# 현재 선택된 텍스트에 대해 글자 모양 설정
if hwp.set_font_style(
font_name=name,
font_size=size,
bold=bold,
italic=italic,
underline=underline,
select_previous_text=select_previous_text
):
logger.info("Successfully set font")
return "Font set successfully"
else:
return "Error: Failed to set font"
except Exception as e:
logger.error(f"Error setting font: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_insert_table(rows: int, cols: int) -> str:
"""Insert a table at the current cursor position."""
try:
# HwpTableTools 인스턴스 가져오기
table_tools = get_hwp_table_tools()
if not table_tools:
return "Error: Failed to get table tools instance"
return table_tools.insert_table(rows, cols)
except Exception as e:
logger.error(f"Error inserting table: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_insert_paragraph() -> str:
"""Insert a new paragraph."""
try:
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
if hwp.insert_paragraph():
logger.info("Successfully inserted paragraph")
return "Paragraph inserted successfully"
else:
return "Error: Failed to insert paragraph"
except Exception as e:
logger.error(f"Error inserting paragraph: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_get_text() -> str:
"""Get the text content of the current document."""
try:
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
text = hwp.get_text()
if text is not None:
logger.info("Successfully retrieved document text")
return text
else:
return "Error: Failed to get document text"
except Exception as e:
logger.error(f"Error getting text: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_close(save: bool = True) -> str:
"""Close the HWP document and connection."""
try:
global hwp_controller, hwp_table_tools
if hwp_controller and hwp_controller.is_hwp_running:
if hwp_controller.disconnect():
logger.info("Successfully closed HWP connection")
hwp_controller = None
hwp_table_tools = None
return "HWP connection closed successfully"
else:
return "Error: Failed to close HWP connection"
else:
return "HWP is already closed"
except Exception as e:
logger.error(f"Error closing HWP: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_ping_pong(message: str = "핑") -> str:
"""
핑퐁 테스트용 함수입니다. 핑을 보내면 퐁을 응답하고, 퐁을 보내면 핑을 응답합니다.
Args:
message: 테스트 메시지 (기본값: "핑")
Returns:
str: 응답 메시지
"""
try:
logger.info(f"핑퐁 테스트 함수 호출됨: 메시지 - {message}")
# 메시지에 따라 응답 생성
if message == "핑":
response = "퐁"
elif message == "퐁":
response = "핑"
else:
response = f"모르는 메시지입니다: {message} (핑 또는 퐁을 보내주세요)"
# 응답 생성 시간 기록
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
# 응답 데이터 구성
result = {
"response": response,
"original_message": message,
"timestamp": current_time
}
return json.dumps(result, ensure_ascii=False)
except Exception as e:
logger.error(f"핑퐁 테스트 함수 오류: {str(e)}", exc_info=True)
return f"테스트 오류 발생: {str(e)}"
@mcp.tool()
def hwp_create_table_with_data(rows: int, cols: int, data = None, has_header: bool = False) -> str:
"""
pywin32를 사용하여 현재 커서 위치에 표를 생성하고 데이터를 채웁니다.
Args:
rows: 표의 행 수
cols: 표의 열 수
data: 표에 채울 데이터 (JSON 문자열 또는 파이썬 리스트)
has_header: 첫 번째 행을 헤더로 처리할지 여부
Returns:
str: 결과 메시지
"""
try:
# HwpTableTools 인스턴스 가져오기
table_tools = get_hwp_table_tools()
if not table_tools:
return "Error: Failed to get table tools instance"
# 현재 커서가 표 안에 있는지 확인
hwp = get_hwp_controller()
is_in_table = False
try:
hwp.hwp.Run("TableCellBlock")
hwp.hwp.Run("Cancel")
is_in_table = True
except:
is_in_table = False
# 표 안에 있지 않은 경우에만 새 표 생성
if not is_in_table:
# 표 생성
if not table_tools.insert_table(rows, cols):
return "Error: Failed to create table"
# 데이터가 있는 경우 표 채우기
if data is not None:
# 데이터 형식 로깅
logger.info(f"Create table with data type: {type(data)}, data: {str(data)[:100]}...")
# 데이터가 이미 리스트 형태인 경우
if isinstance(data, list):
logger.info("Data is already a list, using directly")
processed_data = data
# 데이터가 문자열인 경우 JSON 파싱 시도
elif isinstance(data, str):
try:
import json
try:
processed_data = json.loads(data)
logger.info(f"Successfully parsed JSON data with {len(processed_data)} rows")
except json.JSONDecodeError as e:
logger.error(f"JSON 파싱 오류: {str(e)}")
try:
import ast
processed_data = ast.literal_eval(data)
logger.info(f"Successfully parsed data with literal_eval")
except Exception as e2:
logger.error(f"리터럴 평가 오류: {str(e2)}")
return f"표는 생성되었으나 JSON 데이터 파싱 오류: {str(e)}"
except Exception as e:
logger.error(f"데이터 파싱 오류: {str(e)}", exc_info=True)
return f"표는 생성되었으나 데이터 파싱 오류: {str(e)}"
else:
return f"표는 생성되었으나 지원되지 않는 데이터 유형: {type(data)}"
# 데이터 구조 유효성 검사
if not isinstance(processed_data, list):
return f"표는 생성되었으나 데이터가 리스트 형식이 아닙니다: {type(processed_data)}"
if len(processed_data) == 0:
return "표는 생성되었으나 데이터 리스트가 비어 있습니다."
# 모든 행이 리스트인지 확인 및 변환
for i, row in enumerate(processed_data):
if not isinstance(row, list):
logger.warning(f"Row {i} is not a list, converting: {row}")
processed_data[i] = [row]
# 모든 데이터를 문자열로 변환
string_data = []
for row in processed_data:
string_row = [str(cell) if cell is not None else "" for cell in row]
string_data.append(string_row)
# 표에 데이터 채우기
if table_tools.fill_table_with_data(string_data, 1, 1, has_header):
return f"표 생성 및 데이터 입력 완료 ({rows}x{cols})"
else:
return "표는 생성되었으나 데이터 입력에 실패했습니다."
return f"표 생성 완료 ({rows}x{cols})"
except Exception as e:
logger.error(f"표 생성 중 오류: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_create_complete_document(document_spec: dict) -> dict:
"""
전체 문서를 한 번의 호출로 작성합니다. 문서 구조, 내용 및 서식을 JSON으로 정의하여 전달합니다.
Args:
document_spec (dict): 문서 사양을 담은 딕셔너리. 다음과 같은 구조를 가집니다:
{
"title": "문서 제목", # 선택 사항
"filename": "저장할_파일명.hwp", # 선택 사항, 저장할 경우
"elements": [ # 필수: 문서를 구성하는 요소 배열
{
"type": "heading", # 요소 유형 (heading, text, paragraph, table, etc.)
"content": "제목", # 요소 내용
"properties": { # 요소 속성 (선택 사항)
"font_size": 16,
"bold": true,
...
}
},
...
],
"special_type": { # 특수 문서 유형 (선택 사항)
"type": "report", # 보고서 등 특수 문서 유형
"params": { ... } # 특수 문서에 필요한 매개변수
},
"save": true # 저장 여부 (선택 사항)
}
Returns:
dict: 문서 생성 결과
"""
try:
hwp = get_hwp_controller()
if not hwp:
return {"status": "error", "message": "Failed to connect to HWP program"}
# 새 문서 생성
if not hwp.create_new_document():
return {"status": "error", "message": "Failed to create new document"}
# 문서 사양 유효성 검사
if not document_spec:
return {"status": "error", "message": "Document specification is required"}
if "special_type" in document_spec:
# 특수 문서 유형 처리 (보고서 등)
special_type = document_spec["special_type"]
special_type_name = special_type.get("type", "")
special_params = special_type.get("params", {})
# 보고서 처리
if special_type_name == "report":
return _create_report(hwp, special_params, document_spec)
# 편지 처리
elif special_type_name == "letter":
return _create_letter(hwp, special_params, document_spec)
else:
return {"status": "error", "message": f"Unknown special document type: {special_type_name}"}
# 일반 문서 처리
elif "elements" in document_spec:
elements = document_spec.get("elements", [])
# 문서 요소 처리
for element in elements:
element_type = element.get("type", "")
content = element.get("content", "")
properties = element.get("properties", {})
# 요소 유형에 따른 처리
if element_type == "heading":
# 제목 스타일 설정
font_size = properties.get("font_size", 16)
bold = properties.get("bold", True)
hwp.set_font(None, font_size, bold, False)
hwp.insert_text(content)
hwp.insert_paragraph()
elif element_type == "text":
# 텍스트 스타일 설정
font_size = properties.get("font_size", 10)
bold = properties.get("bold", False)
italic = properties.get("italic", False)
hwp.set_font(None, font_size, bold, italic)
hwp.insert_text(content)
elif element_type == "paragraph":
hwp.insert_paragraph()
elif element_type == "table":
rows = properties.get("rows", 0)
cols = properties.get("cols", 0)
data = properties.get("data", [])
if rows > 0 and cols > 0:
hwp.insert_table(rows, cols)
# 테이블 데이터 채우기 (구현 필요)
# 현재는 표만 생성하고 데이터는 처리하지 않음
else:
logger.warning(f"Unknown element type: {element_type}")
else:
return {"status": "error", "message": "Document must contain 'elements' or 'special_type'"}
# 문서 저장
if document_spec.get("save", False):
filename = document_spec.get("filename", "generated_document.hwp")
if hwp.save_document(filename):
return {
"status": "success",
"message": "Document created and saved successfully",
"saved_path": filename
}
else:
return {
"status": "partial_success",
"message": "Document created but failed to save"
}
return {"status": "success", "message": "Document created successfully"}
except Exception as e:
logger.error(f"Error creating document: {str(e)}", exc_info=True)
return {"status": "error", "message": f"Error: {str(e)}"}
def _create_report(hwp, params, document_spec):
"""보고서 문서를 생성합니다."""
try:
title = params.get("title", "보고서 제목")
author = params.get("author", "작성자")
date = params.get("date", time.strftime("%Y년 %m월 %d일"))
sections = params.get("sections", [{"title": "섹션 제목", "content": "섹션 내용"}])
# 제목 페이지
hwp.set_font(None, 22, True, False)
hwp.insert_text(title)
hwp.insert_paragraph()
hwp.insert_paragraph()
hwp.set_font(None, 14, False, False)
hwp.insert_text(f"작성자: {author}")
hwp.insert_paragraph()
hwp.insert_text(f"작성일: {date}")
hwp.insert_paragraph()
hwp.insert_paragraph()
# 각 섹션
for section in sections:
section_title = section.get("title", "")
section_content = section.get("content", "")
hwp.set_font(None, 16, True, False)
hwp.insert_text(section_title)
hwp.insert_paragraph()
hwp.set_font(None, 12, False, False)
hwp.insert_text(section_content)
hwp.insert_paragraph()
hwp.insert_paragraph()
# 문서 저장
result = {"status": "success", "message": "Report created successfully"}
if document_spec.get("save", False):
filename = document_spec.get("filename", "report.hwp")
if hwp.save_document(filename):
result["saved_path"] = filename
else:
result["message"] = "Report created but failed to save"
result["status"] = "partial_success"
return result
except Exception as e:
logger.error(f"Error creating report: {str(e)}", exc_info=True)
return {"status": "error", "message": f"Error: {str(e)}"}
def _create_letter(hwp, params, document_spec):
"""편지 문서를 생성합니다."""
try:
title = params.get("title", "제목 없음")
recipient = params.get("recipient", "받는 사람")
content = params.get("content", "내용을 입력하세요.")
sender = params.get("sender", "보내는 사람")
date = params.get("date", time.strftime("%Y년 %m월 %d일"))
# 제목 (굵게, 크게)
hwp.set_font(None, 16, True, False)
hwp.insert_text(title)
hwp.insert_paragraph()
hwp.insert_paragraph()
# 받는 사람
hwp.set_font(None, 12, False, False)
hwp.insert_text(f"받는 사람: {recipient}")
hwp.insert_paragraph()
hwp.insert_paragraph()
# 내용
hwp.set_font(None, 12, False, False)
hwp.insert_text(content)
hwp.insert_paragraph()
hwp.insert_paragraph()
# 날짜 (오른쪽 정렬)
# 오른쪽 정렬은 현재 구현되어 있지 않으므로 공백으로 대체
hwp.set_font(None, 12, False, False)
hwp.insert_text("".ljust(40) + date)
hwp.insert_paragraph()
# 보내는 사람 (오른쪽 정렬, 굵게)
hwp.set_font(None, 12, True, False)
hwp.insert_text("".ljust(40) + sender)
# 문서 저장
result = {"status": "success", "message": "Letter created successfully"}
if document_spec.get("save", False):
filename = document_spec.get("filename", "letter.hwp")
if hwp.save_document(filename):
result["saved_path"] = filename
else:
result["message"] = "Letter created but failed to save"
result["status"] = "partial_success"
return result
except Exception as e:
logger.error(f"Error creating letter: {str(e)}", exc_info=True)
return {"status": "error", "message": f"Error: {str(e)}"}
@mcp.tool()
def hwp_create_document_from_text(content: str, title: str = None, format_content: bool = True, save_filename: str = None, preserve_linebreaks: bool = True) -> dict:
"""
단일 문자열로 된 텍스트 내용으로 문서를 생성합니다.
Args:
content (str): 문서 내용 (형식을 자동으로 감지하고 처리)
title (str, optional): 문서 제목. 없으면 첫 줄을 제목으로 사용.
format_content (bool): 내용 자동 포맷팅 여부 (줄바꿈, 문단 구분 등)
save_filename (str, optional): 저장할 파일 이름. 제공되지 않으면 저장하지 않음.
preserve_linebreaks (bool): 줄바꿈 유지 여부. True이면 원본 텍스트의 모든 줄바꿈 유지.
Returns:
dict: 문서 생성 결과
"""
try:
hwp = get_hwp_controller()
if not hwp:
return {"status": "error", "message": "Failed to connect to HWP program"}
# 새 문서 생성
if not hwp.create_new_document():
return {"status": "error", "message": "Failed to create new document"}
# 내용이 없는 경우
if not content:
return {"status": "error", "message": "Document content is required"}
# 내용을 줄로 분리
lines = content.split('\n')
# 빈 줄을 기준으로 블록 구분
blocks = []
current_block = []
for line in lines:
if line.strip(): # 빈 줄이 아닌 경우
current_block.append(line)
else: # 빈 줄인 경우 블록 구분
if current_block:
blocks.append(current_block)
current_block = []
# 마지막 블록 추가
if current_block:
blocks.append(current_block)
# 제목 처리
if not title and blocks:
# 첫 번째 블록의 첫 번째 줄을 제목으로 사용
title = blocks[0][0]
if len(blocks[0]) > 1:
blocks[0] = blocks[0][1:] # 첫 번째 줄 제거
else:
blocks = blocks[1:] # 첫 번째 블록 제거
# 제목 추가
if title:
# 먼저 폰트 설정 후 텍스트 입력 (수정된 방식)
hwp.set_font(None, 16, True, False)
hwp.insert_text(title)
hwp.insert_paragraph()
hwp.insert_paragraph()
# 내용 자동 포맷팅
if format_content:
# 블록 단위로 처리
for block in blocks:
# 블록 내 첫 번째 줄로 블록 유형 판단
first_line = block[0].strip() if block else ""
# 제목 형식 감지 (예: #으로 시작하면 제목)
if first_line.startswith('#'):
level = 0
for char in first_line:
if char == '#':
level += 1
else:
break
heading_text = first_line[level:].strip()
font_size = max(11, 16 - (level - 1)) # 제목 레벨에 따라 글자 크기 조정
# 먼저 폰트 설정 후 텍스트 입력 (수정된 방식)
hwp.set_font(None, font_size, True, False)
hwp.insert_text(heading_text)
hwp.insert_paragraph()
# 제목 이후의 줄들 처리 (있을 경우)
if len(block) > 1:
hwp.set_font(None, 11, False, False)
for line in block[1:]:
hwp.insert_text(line)
hwp.insert_paragraph()
# 글머리 기호 감지 (예: - 또는 * 으로 시작하면 글머리 기호)
elif first_line.startswith(('-', '*', '•')):
hwp.set_font(None, 11, False, False)
for line in block:
line_stripped = line.strip()
if line_stripped.startswith(('-', '*', '•')):
content_text = line_stripped[1:].strip()
hwp.insert_text(f"• {content_text}")
else:
hwp.insert_text(line_stripped)
hwp.insert_paragraph()
# 시 또는 줄바꿈이 중요한 텍스트 (각 줄을 개별적으로 처리)
elif preserve_linebreaks:
hwp.set_font(None, 11, False, False)
for line in block:
hwp.insert_text(line)
hwp.insert_paragraph()
# 일반 텍스트 (블록 전체를 하나의 단락으로 처리)
else:
hwp.set_font(None, 11, False, False)
block_text = '\n'.join(block)
hwp.insert_text(block_text)
hwp.insert_paragraph()
# 블록 사이에 추가 줄바꿈
hwp.insert_paragraph()
# 자동 포맷팅 없이 그대로 삽입 (줄바꿈 보존)
else:
hwp.set_font(None, 11, False, False)
for line in lines:
if line.strip(): # 내용이 있는 줄
hwp.insert_text(line)
hwp.insert_paragraph() # 빈 줄이든 내용이 있는 줄이든 항상 줄바꿈
# 문서 저장
result = {"status": "success", "message": "Document created from text successfully"}
if save_filename:
if hwp.save_document(save_filename):
result["saved_path"] = save_filename
else:
result["message"] = "Document created but failed to save"
result["status"] = "partial_success"
return result
except Exception as e:
logger.error(f"Error creating document from text: {str(e)}", exc_info=True)
return {"status": "error", "message": f"Error: {str(e)}"}
@mcp.tool()
def hwp_batch_operations(operations: list) -> dict:
"""
여러 HWP 작업을 한 번의 호출로 일괄 처리합니다.
Args:
operations (list): 실행할 작업 목록. 각 작업은 다음 형식의 딕셔너리입니다:
{
"operation": "작업명", # 예: "create", "set_font", "insert_text" 등
"params": {파라미터 딕셔너리} # 해당 작업에 필요한 파라미터
}
Returns:
dict: 각 작업의 실행 결과
"""
try:
hwp = get_hwp_controller()
if not hwp:
return {"status": "error", "message": "Failed to connect to HWP program"}
results = []
for op in operations:
operation = op.get("operation", "")
params = op.get("params", {})
result = {"operation": operation, "status": "success", "message": ""}
try:
if operation == "create":
if hwp.create_new_document():
result["message"] = "New document created successfully"
else:
result["status"] = "error"
result["message"] = "Failed to create new document"
elif operation == "open":
path = params.get("path", "")
if not path:
result["status"] = "error"
result["message"] = "File path is required"
elif hwp.open_document(path):
result["message"] = f"Document opened: {path}"
else:
result["status"] = "error"
result["message"] = "Failed to open document"
elif operation == "save":
path = params.get("path", None)
if path and hwp.save_document(path):
result["message"] = f"Document saved to: {path}"
elif not path:
temp_path = os.path.join(os.getcwd(), "temp_document.hwp")
if hwp.save_document(temp_path):
result["message"] = f"Document saved to: {temp_path}"
result["path"] = temp_path
else:
result["status"] = "error"
result["message"] = "Failed to save document"
else:
result["status"] = "error"
result["message"] = "Failed to save document"
elif operation == "insert_text":
text = params.get("text", "")
preserve_linebreaks = params.get("preserve_linebreaks", True)
if not text:
result["status"] = "error"
result["message"] = "Text is required"
elif preserve_linebreaks and ('\n' in text or '\\n' in text):
# 줄바꿈 보존 처리 개선
# 이스케이프된 줄바꿈 문자(\n)와 실제 줄바꿈 문자 모두 처리
# 먼저 이스케이프된 줄바꿈 문자를 실제 줄바꿈으로 변환
processed_text = text.replace('\\n', '\n')
lines = processed_text.split('\n')
success = True
for i, line in enumerate(lines):
if not hwp.insert_text(line):
success = False
break
# 마지막 줄이 아니면 줄바꿈 삽입
if i < len(lines) - 1:
hwp.insert_paragraph()
if success:
result["message"] = "Text with line breaks inserted successfully"
else:
result["status"] = "error"
result["message"] = "Failed to insert text with line breaks"
elif hwp.insert_text(text):
result["message"] = "Text inserted successfully"
else:
result["status"] = "error"
result["message"] = "Failed to insert text"
elif operation == "set_font":
name = params.get("name", None)
size = params.get("size", None)
bold = params.get("bold", False)
italic = params.get("italic", False)
underline = params.get("underline", False)
select_previous_text = params.get("select_previous_text", False)
if hwp.set_font_style(font_name=name, font_size=size, bold=bold, italic=italic, underline=underline, select_previous_text=select_previous_text):
result["message"] = "Font set successfully"
else:
result["status"] = "error"
result["message"] = "Failed to set font"
elif operation == "insert_paragraph":
count = params.get("count", 1) # 여러 줄 삽입 가능
success = True
for _ in range(count):
if not hwp.insert_paragraph():
success = False
break
if success:
result["message"] = f"{count} paragraph(s) inserted successfully"
else:
result["status"] = "error"
result["message"] = "Failed to insert paragraph"
elif operation == "insert_table":
rows = params.get("rows", 0)
cols = params.get("cols", 0)
data = params.get("data", [])
has_header = params.get("has_header", False)
table_tools = get_hwp_table_tools()
if not table_tools:
result["status"] = "error"
result["message"] = "Failed to get table tools instance"
elif rows <= 0 or cols <= 0:
result["status"] = "error"
result["message"] = "Valid rows and cols are required"
else:
# 데이터가 있으면 테이블 생성 후 데이터 채우기
if data:
resp = table_tools.create_table_with_data(rows, cols, json.dumps(data) if isinstance(data, list) else data, has_header)
result["message"] = resp
if resp.startswith("Error"):
result["status"] = "error"
else:
resp = table_tools.insert_table(rows, cols)
result["message"] = resp
if resp.startswith("Error"):
result["status"] = "error"
elif operation == "set_table_cell_text":
row = params.get("row", 0)
col = params.get("col", 0)
text = params.get("text", "")
table_tools = get_hwp_table_tools()
if not table_tools:
result["status"] = "error"
result["message"] = "Failed to get table tools instance"
elif row <= 0 or col <= 0:
result["status"] = "error"
result["message"] = "Valid row and col are required"
else:
resp = table_tools.set_cell_text(row, col, text)
result["message"] = resp
if resp.startswith("Error"):
result["status"] = "error"
elif operation == "merge_table_cells":
start_row = params.get("start_row", 0)
start_col = params.get("start_col", 0)
end_row = params.get("end_row", 0)
end_col = params.get("end_col", 0)
table_tools = get_hwp_table_tools()
if not table_tools:
result["status"] = "error"
result["message"] = "Failed to get table tools instance"
elif start_row <= 0 or start_col <= 0 or end_row <= 0 or end_col <= 0:
result["status"] = "error"
result["message"] = "Valid cell coordinates are required"
else:
resp = table_tools.merge_cells(start_row, start_col, end_row, end_col)
result["message"] = resp
if resp.startswith("Error"):
result["status"] = "error"
elif operation == "get_text":
text = hwp.get_text()
if text is not None:
result["message"] = "Text retrieved successfully"
result["text"] = text
else:
result["status"] = "error"
result["message"] = "Failed to retrieve text"
elif operation == "close":
save = params.get("save", True)
if hwp.disconnect():
result["message"] = "Document closed successfully"
# 전역 변수 초기화
global hwp_controller
hwp_controller = None
else:
result["status"] = "error"
result["message"] = "Failed to close document"
# 새로 추가: 문서 한 번에 생성
elif operation == "create_document_from_text":
content = params.get("content", "")
title = params.get("title", None)
format_content = params.get("format_content", True)
save_filename = params.get("save_filename", None)
preserve_linebreaks = params.get("preserve_linebreaks", True)
if not content:
result["status"] = "error"
result["message"] = "Document content is required"
else:
# 내부적으로 기존 함수 호출
doc_result = hwp_create_document_from_text(
content=content,
title=title,
format_content=format_content,
save_filename=save_filename,
preserve_linebreaks=preserve_linebreaks
)
result["status"] = doc_result.get("status", "error")
result["message"] = doc_result.get("message", "Unknown error")
if "saved_path" in doc_result:
result["saved_path"] = doc_result["saved_path"]
else:
result["status"] = "error"
result["message"] = f"Unknown operation: {operation}"
except Exception as e:
result["status"] = "error"
result["message"] = f"Error in operation '{operation}': {str(e)}"
results.append(result)
return {"status": "success", "results": results}
except Exception as e:
logger.error(f"Error in batch operations: {str(e)}", exc_info=True)
return {"status": "error", "message": f"Error: {str(e)}"}
@mcp.tool()
def hwp_fill_table_with_data(data, start_row: int = 1, start_col: int = 1, has_header: bool = False) -> str:
"""
이미 존재하는 표에 데이터를 채웁니다.
Args:
data: 표에 채울 데이터 (JSON 문자열 또는 2차원 리스트)
start_row: 시작 행 번호 (1부터 시작)
start_col: 시작 열 번호 (1부터 시작)
has_header: 첫 번째 행을 헤더로 처리할지 여부
Returns:
str: 결과 메시지
"""
try:
table_tools = get_hwp_table_tools()
if not table_tools:
return "Error: Failed to get table tools instance"
# 데이터 형식 로깅
logger.info(f"Received data type: {type(data)}, data: {str(data)[:100]}...")
# 데이터 처리
processed_data = []
# 이미 리스트 형태인 경우
if isinstance(data, list):
logger.info("Data is already a list, processing directly")
processed_data = data
# 문자열인 경우 JSON 파싱 시도
elif isinstance(data, str):
try:
import json
# JSON 파싱 시도
try:
processed_data = json.loads(data)
logger.info(f"Successfully parsed JSON data with {len(processed_data)} rows")
except json.JSONDecodeError as e:
logger.error(f"JSON 디코딩 오류: {str(e)}")
# 특수 케이스: 1부터 10까지 세로로 채우는 요청인 경우
if "1부터 10까지" in data and "세로" in data:
logger.info("특수 케이스 감지: 1부터 10까지 세로로 채우기")
processed_data = []
for i in range(1, 11):
processed_data.append([str(i)])
else:
# 마지막 시도: 리터럴 평가
try:
import ast
processed_data = ast.literal_eval(data)
logger.info(f"Successfully parsed data with literal_eval: {len(processed_data)} rows")
except:
# 단순 문자열을 직접 파싱
try:
# 문자열에서 쉼표로 구분된 항목 추출 시도
if "," in data:
items = [item.strip() for item in data.split(",")]
processed_data = [[item] for item in items]
else:
# 단일 값인 경우
processed_data = [[data]]
except Exception as parse_err:
return f"Error: Failed to parse string data - {str(parse_err)}"
except Exception as e:
logger.error(f"데이터 파싱 오류: {str(e)}", exc_info=True)
return f"Error: Failed to parse data - {str(e)}"
else:
return f"Error: Unsupported data type: {type(data)}"
# 데이터 구조 유효성 검사
if not isinstance(processed_data, list):
logger.error(f"Processed data is not a list: {type(processed_data)}")
return f"Error: Data must be a list, got {type(processed_data)}"
if len(processed_data) == 0:
logger.error("Empty data list")
return "Error: Empty data list"
# 모든 행이 리스트인지 확인 및 변환
for i, row in enumerate(processed_data):
if not isinstance(row, list):
logger.warning(f"Row {i} is not a list, converting to list: {row}")
processed_data[i] = [row] # 리스트가 아닌 항목을 리스트로 변환
# 모든 데이터를 문자열로 변환
final_data = []
for row in processed_data:
final_row = [str(cell) if cell is not None else "" for cell in row]
final_data.append(final_row)
logger.info(f"Final processed data has {len(final_data)} rows")
# 표에 데이터 채우기
result = table_tools.fill_table_with_data(final_data, start_row, start_col, has_header)
logger.info(f"Table filling result: {result}")
return result
except Exception as e:
logger.error(f"표 데이터 입력 중 오류: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
def hwp_fill_column_numbers(start: int = 1, end: int = 10, column: int = 1, from_first_cell: bool = True) -> str:
"""
표의 특정 열에 시작 숫자부터 끝 숫자까지 세로로 채웁니다.
Args:
start: 시작 숫자 (기본값: 1)
end: 끝 숫자 (기본값: 10)
column: 숫자를 채울 열 번호 (1부터 시작, 기본값: 1)
from_first_cell: 정확히 표의 첫 번째 셀부터 시작할지 여부 (기본값: True)
Returns:
str: 결과 메시지
"""
try:
# HWP 컨트롤러 가져오기
hwp = get_hwp_controller()
if not hwp:
return "Error: Failed to connect to HWP program"
# 표 선택 (현재 커서 위치에 표가 있어야 함)
logger.info(f"테이블 열에 숫자 채우기: 열 {column}, {start}부터 {end}까지")
# 표의 첫 번째 셀로 이동 (문서의 표 맨 앞)
hwp.hwp.Run("TableColBegin")
# from_first_cell이 False인 경우에만 아래로 이동
if not from_first_cell:
hwp.hwp.Run("TableLowerCell")
# 지정된 열로 이동
for _ in range(column - 1):
hwp.hwp.Run("TableRightCell")
# 각 행에 숫자 채우기
for num in range(start, end + 1):
# 셀 선택 및 내용 지우기
hwp.hwp.Run("Select")
hwp.hwp.Run("Delete")
# 셀에 숫자 입력
hwp.insert_text(str(num))
# 다음 행으로 이동 (마지막 행이 아닌 경우)
if num < end:
hwp.hwp.Run("TableLowerCell")
logger.info(f"테이블 열({column})에 숫자 {start}~{end} 입력 완료")
return f"테이블 열({column})에 숫자 {start}~{end} 입력 완료"
except Exception as e:
logger.error(f"테이블 숫자 채우기 오류: {str(e)}", exc_info=True)
return f"Error: {str(e)}"
if __name__ == "__main__":
logger.info("Starting HWP MCP stdio server")
try:
# Run the FastMCP server with stdio transport
mcp.run(transport="stdio")
except Exception as e:
logger.error(f"Error running server: {str(e)}", exc_info=True)
print(f"Error: {str(e)}", file=sys.stderr)
sys.exit(1)