claude-code-hwp-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +409 -0
  2. package/dist/hwp-bridge.d.ts +67 -0
  3. package/dist/hwp-bridge.js +320 -0
  4. package/dist/hwpx-engine.d.ts +39 -0
  5. package/dist/hwpx-engine.js +187 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +54 -0
  8. package/dist/prompts/hwp-prompts.d.ts +2 -0
  9. package/dist/prompts/hwp-prompts.js +368 -0
  10. package/dist/resources/document-resources.d.ts +3 -0
  11. package/dist/resources/document-resources.js +109 -0
  12. package/dist/server.d.ts +12 -0
  13. package/dist/server.js +29 -0
  14. package/dist/tools/analysis-tools.d.ts +4 -0
  15. package/dist/tools/analysis-tools.js +414 -0
  16. package/dist/tools/composite-tools.d.ts +3 -0
  17. package/dist/tools/composite-tools.js +664 -0
  18. package/dist/tools/document-tools.d.ts +3 -0
  19. package/dist/tools/document-tools.js +264 -0
  20. package/dist/tools/editing-tools.d.ts +4 -0
  21. package/dist/tools/editing-tools.js +916 -0
  22. package/package.json +31 -0
  23. package/python/__pycache__/hwp_analyzer.cpython-313.pyc +0 -0
  24. package/python/__pycache__/hwp_editor.cpython-313.pyc +0 -0
  25. package/python/__pycache__/hwp_service.cpython-313.pyc +0 -0
  26. package/python/__pycache__/privacy_scanner.cpython-313.pyc +0 -0
  27. package/python/__pycache__/ref_reader.cpython-313.pyc +0 -0
  28. package/python/__pycache__/test_integration.cpython-313.pyc +0 -0
  29. package/python/hwp_analyzer.py +544 -0
  30. package/python/hwp_editor.py +933 -0
  31. package/python/hwp_service.py +1291 -0
  32. package/python/privacy_scanner.py +115 -0
  33. package/python/ref_reader.py +115 -0
  34. package/python/requirements.txt +2 -0
@@ -0,0 +1,916 @@
1
+ /**
2
+ * Editing tools: fill fields, fill table cells, find/replace, insert text
3
+ */
4
+ import { z } from 'zod';
5
+ import path from 'node:path';
6
+ const FILL_TIMEOUT = 60000;
7
+ export function registerEditingTools(server, bridge, toolset = 'standard') {
8
+ // --- standard 이상에서만: fill_fields ---
9
+ if (toolset !== 'minimal') {
10
+ server.tool('hwp_fill_fields', '문서의 필드(양식)에 값을 채웁니다. 반드시 먼저 hwp_get_fields로 필드 이름을 확인한 후 사용하세요. 필드가 없는 문서에는 hwp_fill_table_cells나 hwp_insert_text를 사용하세요.', {
11
+ file_path: z.string().optional().describe('HWP 파일 경로 (생략 시 현재 열린 문서)'),
12
+ fields: z.record(z.string(), z.string()).describe('채울 필드 객체 { "필드이름": "값" }'),
13
+ }, async ({ file_path, fields }) => {
14
+ try {
15
+ await bridge.ensureRunning();
16
+ const params = { fields };
17
+ if (file_path) {
18
+ params.file_path = path.resolve(file_path);
19
+ }
20
+ const response = await bridge.send('fill_document', params, FILL_TIMEOUT);
21
+ if (!response.success) {
22
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
23
+ }
24
+ bridge.setCachedAnalysis(null);
25
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
26
+ }
27
+ catch (err) {
28
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
29
+ }
30
+ });
31
+ } // end fill_fields (standard+)
32
+ // --- minimal에 포함: fill_table_cells, find_replace, insert_text ---
33
+ server.tool('hwp_fill_table_cells', '문서의 표 셀에 값을 채웁니다. 반드시 먼저 hwp_get_tables로 표 구조를 확인하세요. 병합 셀이 있으면 label 파라미터로 라벨 텍스트 기반 매칭을 추천합니다. 예: {label: "계약금액", text: "50,000,000원"}. tab이나 row/col도 사용 가능합니다.', {
34
+ file_path: z.string().optional().describe('HWP 파일 경로 (생략 시 현재 열린 문서)'),
35
+ tables: z.array(z.object({
36
+ index: z.number().int().min(0).describe('표 인덱스 (0부터 시작)'),
37
+ cells: z.array(z.object({
38
+ row: z.number().int().min(0).optional().describe('행 번호 (0부터, row/col 방식)'),
39
+ col: z.number().int().min(0).optional().describe('열 번호 (0부터, row/col 방식)'),
40
+ tab: z.number().int().min(0).optional().describe('Tab 인덱스 (0부터, 병합 셀용). hwp_map_table_cells로 확인'),
41
+ label: z.string().optional().describe('라벨 텍스트로 셀 찾기 (예: "계약금액"). 해당 라벨 오른쪽 셀에 값을 채움'),
42
+ row_label: z.string().optional().describe('행 라벨 텍스트 (예: "전체기간"). label과 함께 사용하면 교차점 셀을 찾음'),
43
+ direction: z.enum(['right', 'below']).optional().default('right').describe('라벨 기준 값 셀 방향'),
44
+ text: z.string().describe('채울 텍스트'),
45
+ style: z.object({
46
+ color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('글자 색상 [R, G, B]'),
47
+ bold: z.boolean().optional().describe('굵게'),
48
+ italic: z.boolean().optional().describe('기울임'),
49
+ underline: z.boolean().optional().describe('밑줄'),
50
+ font_size: z.number().positive().optional().describe('글자 크기 (pt)'),
51
+ font_name: z.string().optional().describe('글꼴 이름'),
52
+ char_spacing: z.number().optional().describe('자간 (%)'),
53
+ width_ratio: z.number().optional().describe('장평 (%)'),
54
+ }).optional().describe('셀별 서식 (생략 시 기존 셀 서식 상속)'),
55
+ })).describe('채울 셀 목록. label, tab, row+col 중 하나 필수'),
56
+ })).describe('채울 표 배열'),
57
+ }, async ({ file_path, tables }) => {
58
+ try {
59
+ await bridge.ensureRunning();
60
+ // Check if any cell uses tab-based navigation
61
+ const results = [];
62
+ for (const table of tables) {
63
+ // Split cells into three groups: label, tab, row/col
64
+ const labelCells = table.cells.filter(c => c.label !== undefined);
65
+ const tabCells = table.cells.filter(c => c.label === undefined && c.tab !== undefined);
66
+ const rowColCells = table.cells.filter(c => c.label === undefined && c.tab === undefined && c.row !== undefined && c.col !== undefined);
67
+ // Label-based cells → fill_by_label
68
+ if (labelCells.length > 0) {
69
+ const resp = await bridge.send('fill_by_label', {
70
+ table_index: table.index,
71
+ cells: labelCells.map(c => ({ label: c.label, text: c.text, direction: c.direction ?? 'right', ...(c.row_label ? { row_label: c.row_label } : {}) })),
72
+ }, FILL_TIMEOUT);
73
+ if (!resp.success) {
74
+ return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
75
+ }
76
+ results.push(resp.data);
77
+ }
78
+ // Tab-based cells → fill_by_tab
79
+ if (tabCells.length > 0) {
80
+ const resp = await bridge.send('fill_by_tab', {
81
+ table_index: table.index,
82
+ cells: tabCells.map(c => ({ tab: c.tab, text: c.text, ...(c.style ? { style: c.style } : {}) })),
83
+ }, FILL_TIMEOUT);
84
+ if (!resp.success) {
85
+ return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
86
+ }
87
+ results.push(resp.data);
88
+ }
89
+ // Row/col-based cells → fill_document (legacy)
90
+ if (rowColCells.length > 0) {
91
+ const params = {
92
+ tables: [{ index: table.index, cells: rowColCells }],
93
+ };
94
+ if (file_path)
95
+ params.file_path = path.resolve(file_path);
96
+ const resp = await bridge.send('fill_document', params, FILL_TIMEOUT);
97
+ if (!resp.success) {
98
+ return { content: [{ type: 'text', text: JSON.stringify({ error: resp.error }) }], isError: true };
99
+ }
100
+ results.push(resp.data);
101
+ }
102
+ }
103
+ bridge.setCachedAnalysis(null);
104
+ // Merge results
105
+ const merged = { filled: 0, failed: 0, errors: [] };
106
+ for (const r of results) {
107
+ const d = r;
108
+ merged.filled += d.filled ?? 0;
109
+ merged.failed += d.failed ?? 0;
110
+ if (d.errors)
111
+ merged.errors.push(...d.errors);
112
+ }
113
+ return { content: [{ type: 'text', text: JSON.stringify(merged) }] };
114
+ }
115
+ catch (err) {
116
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
117
+ }
118
+ });
119
+ server.tool('hwp_find_replace', '문서 전체에서 텍스트를 찾아 바꿉니다. use_regex=true로 정규식 패턴도 사용 가능합니다.', {
120
+ find: z.string().describe('찾을 텍스트 (use_regex=true 시 정규식 패턴)'),
121
+ replace: z.string().describe('바꿀 텍스트'),
122
+ use_regex: z.boolean().optional().describe('정규식 사용 여부 (기본: false)'),
123
+ }, async ({ find, replace, use_regex }) => {
124
+ if (!bridge.getCurrentDocument()) {
125
+ return { content: [{ type: 'text', text: JSON.stringify({
126
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
127
+ hint: 'Python 프로세스가 재시작되면 열린 문서 상태가 초기화됩니다.',
128
+ }) }], isError: true };
129
+ }
130
+ try {
131
+ await bridge.ensureRunning();
132
+ const params = { find, replace };
133
+ if (use_regex)
134
+ params.use_regex = true;
135
+ const response = await bridge.send('find_replace', params, FILL_TIMEOUT);
136
+ if (!response.success) {
137
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
138
+ }
139
+ bridge.setCachedAnalysis(null);
140
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
141
+ }
142
+ catch (err) {
143
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
144
+ }
145
+ });
146
+ // --- standard 이상에서만: find_replace_multi, find_and_append ---
147
+ if (toolset !== 'minimal') {
148
+ server.tool('hwp_find_replace_multi', '여러 건의 찾기/바꾸기를 일괄 실행합니다. use_regex=true로 정규식도 가능.', {
149
+ replacements: z.array(z.object({
150
+ find: z.string().describe('찾을 텍스트'),
151
+ replace: z.string().describe('바꿀 텍스트'),
152
+ })).describe('치환 목록'),
153
+ use_regex: z.boolean().optional().describe('정규식 사용 여부 (기본: false)'),
154
+ }, async ({ replacements, use_regex }) => {
155
+ if (!bridge.getCurrentDocument()) {
156
+ return { content: [{ type: 'text', text: JSON.stringify({
157
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
158
+ }) }], isError: true };
159
+ }
160
+ try {
161
+ await bridge.ensureRunning();
162
+ const params = { replacements };
163
+ if (use_regex)
164
+ params.use_regex = true;
165
+ const response = await bridge.send('find_replace_multi', params, FILL_TIMEOUT);
166
+ if (!response.success) {
167
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
168
+ }
169
+ bridge.setCachedAnalysis(null);
170
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
171
+ }
172
+ catch (err) {
173
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
174
+ }
175
+ });
176
+ server.tool('hwp_find_and_append', '문서에서 텍스트를 찾은 후 그 뒤에 텍스트를 추가합니다. 색상 지정 가능. 기존 텍스트 서식을 보존하면서 새 텍스트를 추가할 때 사용하세요.', {
177
+ find: z.string().describe('찾을 텍스트'),
178
+ append_text: z.string().describe('찾은 텍스트 뒤에 추가할 텍스트'),
179
+ color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('텍스트 색상 [R, G, B] (0-255)'),
180
+ }, async ({ find, append_text, color }) => {
181
+ if (!bridge.getCurrentDocument()) {
182
+ return { content: [{ type: 'text', text: JSON.stringify({
183
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
184
+ }) }], isError: true };
185
+ }
186
+ try {
187
+ await bridge.ensureRunning();
188
+ const params = { find, append_text };
189
+ if (color)
190
+ params.color = color;
191
+ const response = await bridge.send('find_and_append', params);
192
+ if (!response.success) {
193
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
194
+ }
195
+ bridge.setCachedAnalysis(null);
196
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
197
+ }
198
+ catch (err) {
199
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
200
+ }
201
+ });
202
+ } // end find_replace_multi, find_and_append (standard+)
203
+ // --- minimal에 포함: insert_text ---
204
+ server.tool('hwp_insert_text', '현재 커서 위치에 텍스트를 삽입합니다. 필드가 없는 문서에 텍스트를 추가할 때 사용하세요. style로 글꼴/크기/굵기/색상 등 서식 지정 가능.', {
205
+ text: z.string().describe('삽입할 텍스트'),
206
+ color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('텍스트 색상 [R, G, B] (0-255). style.color와 동일 (하위 호환)'),
207
+ style: z.object({
208
+ color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('글자 색상 [R, G, B]'),
209
+ bold: z.boolean().optional().describe('굵게'),
210
+ italic: z.boolean().optional().describe('기울임'),
211
+ underline: z.boolean().optional().describe('밑줄'),
212
+ font_size: z.number().positive().optional().describe('글자 크기 (pt)'),
213
+ font_name: z.string().optional().describe('글꼴 이름 (예: "맑은 고딕")'),
214
+ bg_color: z.array(z.number().int().min(0).max(255)).length(3).optional().describe('배경 색상 [R, G, B]'),
215
+ strikeout: z.boolean().optional().describe('취소선'),
216
+ char_spacing: z.number().optional().describe('자간 (%, 기본 0. 음수=좁게, 양수=넓게)'),
217
+ width_ratio: z.number().optional().describe('장평 (%, 기본 100. 100 미만=좁게, 100 초과=넓게)'),
218
+ font_name_hanja: z.string().optional().describe('한자 글꼴 이름'),
219
+ font_name_japanese: z.string().optional().describe('일본어 글꼴 이름'),
220
+ }).optional().describe('텍스트 서식 옵션'),
221
+ }, async ({ text, color, style }) => {
222
+ if (!bridge.getCurrentDocument()) {
223
+ return { content: [{ type: 'text', text: JSON.stringify({
224
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
225
+ hint: 'Python 프로세스가 재시작되면 열린 문서 상태가 초기화됩니다.',
226
+ }) }], isError: true };
227
+ }
228
+ try {
229
+ await bridge.ensureRunning();
230
+ const params = { text };
231
+ if (style)
232
+ params.style = style;
233
+ else if (color)
234
+ params.color = color;
235
+ const response = await bridge.send('insert_text', params);
236
+ if (!response.success) {
237
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
238
+ }
239
+ bridge.setCachedAnalysis(null);
240
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
241
+ }
242
+ catch (err) {
243
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
244
+ }
245
+ });
246
+ // --- standard 이상에서만: set_paragraph_style, find_replace_nth, insert_picture ---
247
+ if (toolset !== 'minimal') {
248
+ server.tool('hwp_set_paragraph_style', '현재 커서 위치의 단락 서식을 변경합니다. left_margin=나머지줄 시작위치, indent=첫줄 들여쓰기. 첫줄 시작위치 = left_margin + indent.', {
249
+ align: z.enum(['left', 'center', 'right', 'justify']).optional().describe('정렬'),
250
+ line_spacing: z.number().optional().describe('줄간격 (%, 예: 160)'),
251
+ line_spacing_type: z.number().int().min(0).max(2).optional().describe('줄간격 타입 (0=퍼센트)'),
252
+ space_before: z.number().optional().describe('문단 앞 간격 (pt)'),
253
+ space_after: z.number().optional().describe('문단 뒤 간격 (pt)'),
254
+ indent: z.number().optional().describe('첫 줄 들여쓰기 (pt, 양수=들여쓰기, 음수=내어쓰기)'),
255
+ left_margin: z.number().optional().describe('왼쪽 여백/나머지 줄 시작위치 (pt)'),
256
+ right_margin: z.number().optional().describe('오른쪽 여백 (pt)'),
257
+ }, async ({ align, line_spacing, line_spacing_type, space_before, space_after, indent, left_margin, right_margin }) => {
258
+ if (!bridge.getCurrentDocument()) {
259
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
260
+ }
261
+ try {
262
+ await bridge.ensureRunning();
263
+ const style = {};
264
+ if (align)
265
+ style.align = align;
266
+ if (line_spacing !== undefined)
267
+ style.line_spacing = line_spacing;
268
+ if (line_spacing_type !== undefined)
269
+ style.line_spacing_type = line_spacing_type;
270
+ if (space_before !== undefined)
271
+ style.space_before = space_before;
272
+ if (space_after !== undefined)
273
+ style.space_after = space_after;
274
+ if (indent !== undefined)
275
+ style.indent = indent;
276
+ if (left_margin !== undefined)
277
+ style.left_margin = left_margin;
278
+ if (right_margin !== undefined)
279
+ style.right_margin = right_margin;
280
+ const response = await bridge.send('set_paragraph_style', { style });
281
+ if (!response.success) {
282
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
283
+ }
284
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
285
+ }
286
+ catch (err) {
287
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
288
+ }
289
+ });
290
+ server.tool('hwp_find_replace_nth', '문서에서 N번째로 나타나는 텍스트만 치환합니다. 같은 텍스트가 여러 곳에 있을 때 특정 위치만 바꿀 때 사용하세요. AllReplace는 전체를 바꾸지만 이 도구는 지정한 N번째만 바꿉니다.', {
291
+ find: z.string().describe('찾을 텍스트'),
292
+ replace: z.string().describe('바꿀 텍스트'),
293
+ nth: z.number().int().min(1).describe('몇 번째 매칭을 치환할지 (1부터 시작)'),
294
+ }, async ({ find, replace, nth }) => {
295
+ if (!bridge.getCurrentDocument()) {
296
+ return { content: [{ type: 'text', text: JSON.stringify({
297
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
298
+ }) }], isError: true };
299
+ }
300
+ try {
301
+ await bridge.ensureRunning();
302
+ const response = await bridge.send('find_replace_nth', { find, replace, nth });
303
+ if (!response.success) {
304
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
305
+ }
306
+ bridge.setCachedAnalysis(null);
307
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
308
+ }
309
+ catch (err) {
310
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
311
+ }
312
+ });
313
+ server.tool('hwp_insert_picture', '현재 커서 위치에 이미지를 삽입합니다. 표 셀 안에서도 사용 가능합니다. 사업계획서의 제품사진 등을 삽입할 때 사용하세요.', {
314
+ file_path: z.string().describe('이미지 파일 경로 (jpg, png, bmp 등)'),
315
+ width: z.number().min(0).optional().describe('가로 크기 (mm, 0이면 원본 크기)'),
316
+ height: z.number().min(0).optional().describe('세로 크기 (mm, 0이면 원본 크기)'),
317
+ }, async ({ file_path, width, height }) => {
318
+ if (!bridge.getCurrentDocument()) {
319
+ return { content: [{ type: 'text', text: JSON.stringify({
320
+ error: '열린 문서가 없습니다. hwp_open_document로 문서를 열어주세요.',
321
+ }) }], isError: true };
322
+ }
323
+ try {
324
+ await bridge.ensureRunning();
325
+ const params = { file_path: path.resolve(file_path) };
326
+ if (width)
327
+ params.width = width;
328
+ if (height)
329
+ params.height = height;
330
+ const response = await bridge.send('insert_picture', params);
331
+ if (!response.success) {
332
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
333
+ }
334
+ bridge.setCachedAnalysis(null);
335
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
336
+ }
337
+ catch (err) {
338
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
339
+ }
340
+ });
341
+ server.tool('hwp_table_add_row', '표에 행을 추가합니다. 표의 현재 마지막 행 아래에 새 행이 추가됩니다.', {
342
+ table_index: z.number().int().min(0).describe('표 인덱스 (0부터)'),
343
+ }, async ({ table_index }) => {
344
+ if (!bridge.getCurrentDocument()) {
345
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
346
+ }
347
+ try {
348
+ await bridge.ensureRunning();
349
+ const response = await bridge.send('table_add_row', { table_index }, FILL_TIMEOUT);
350
+ if (!response.success) {
351
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
352
+ }
353
+ bridge.setCachedAnalysis(null);
354
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
355
+ }
356
+ catch (err) {
357
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
358
+ }
359
+ });
360
+ server.tool('hwp_insert_markdown', '마크다운 텍스트를 한글 서식으로 변환하여 현재 커서 위치에 삽입합니다. # 제목, **굵게**, - 목록 등을 지원합니다.', {
361
+ text: z.string().describe('마크다운 텍스트'),
362
+ }, async ({ text }) => {
363
+ if (!bridge.getCurrentDocument()) {
364
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
365
+ }
366
+ try {
367
+ await bridge.ensureRunning();
368
+ const response = await bridge.send('insert_markdown', { text }, FILL_TIMEOUT);
369
+ if (!response.success) {
370
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
371
+ }
372
+ bridge.setCachedAnalysis(null);
373
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
374
+ }
375
+ catch (err) {
376
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
377
+ }
378
+ });
379
+ server.tool('hwp_insert_page_break', '현재 커서 위치에 페이지 나누기를 삽입합니다.', {}, async () => {
380
+ if (!bridge.getCurrentDocument()) {
381
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
382
+ }
383
+ try {
384
+ await bridge.ensureRunning();
385
+ const response = await bridge.send('insert_page_break', {});
386
+ if (!response.success) {
387
+ return { content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }], isError: true };
388
+ }
389
+ return { content: [{ type: 'text', text: JSON.stringify(response.data) }] };
390
+ }
391
+ catch (err) {
392
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
393
+ }
394
+ });
395
+ server.tool('hwp_table_delete_row', '표에서 현재 행을 삭제합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
396
+ if (!bridge.getCurrentDocument())
397
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
398
+ try {
399
+ await bridge.ensureRunning();
400
+ const r = await bridge.send('table_delete_row', { table_index }, FILL_TIMEOUT);
401
+ if (!r.success)
402
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
403
+ bridge.setCachedAnalysis(null);
404
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
405
+ }
406
+ catch (err) {
407
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
408
+ }
409
+ });
410
+ server.tool('hwp_table_add_column', '표에 열을 추가합니다 (현재 열 오른쪽).', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
411
+ if (!bridge.getCurrentDocument())
412
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
413
+ try {
414
+ await bridge.ensureRunning();
415
+ const r = await bridge.send('table_add_column', { table_index }, FILL_TIMEOUT);
416
+ if (!r.success)
417
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
418
+ bridge.setCachedAnalysis(null);
419
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
420
+ }
421
+ catch (err) {
422
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
423
+ }
424
+ });
425
+ server.tool('hwp_table_delete_column', '표에서 현재 열을 삭제합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
426
+ if (!bridge.getCurrentDocument())
427
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
428
+ try {
429
+ await bridge.ensureRunning();
430
+ const r = await bridge.send('table_delete_column', { table_index }, FILL_TIMEOUT);
431
+ if (!r.success)
432
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
433
+ bridge.setCachedAnalysis(null);
434
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
435
+ }
436
+ catch (err) {
437
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
438
+ }
439
+ });
440
+ server.tool('hwp_table_merge_cells', '표에서 선택된 셀들을 병합합니다. 먼저 표에 진입한 상태여야 합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
441
+ if (!bridge.getCurrentDocument())
442
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
443
+ try {
444
+ await bridge.ensureRunning();
445
+ const r = await bridge.send('table_merge_cells', { table_index }, FILL_TIMEOUT);
446
+ if (!r.success)
447
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
448
+ bridge.setCachedAnalysis(null);
449
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
450
+ }
451
+ catch (err) {
452
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
453
+ }
454
+ });
455
+ server.tool('hwp_table_create_from_data', '2D 배열 데이터로 새 표를 생성합니다. 현재 커서 위치에 표가 삽입됩니다. header_style=true로 첫 행을 자동 스타일링(Bold+배경색)합니다.', {
456
+ data: z.array(z.array(z.string())).describe('2D 배열 데이터 [["헤더1","헤더2"],["값1","값2"]]'),
457
+ header_style: z.boolean().optional().describe('첫 행을 헤더로 자동 스타일링 (Bold+연회색 배경, 기본 false)'),
458
+ }, async ({ data, header_style }) => {
459
+ if (!bridge.getCurrentDocument())
460
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
461
+ try {
462
+ await bridge.ensureRunning();
463
+ const params = { data };
464
+ if (header_style)
465
+ params.header_style = header_style;
466
+ const r = await bridge.send('table_create_from_data', params, FILL_TIMEOUT);
467
+ if (!r.success)
468
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
469
+ bridge.setCachedAnalysis(null);
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
+ server.tool('hwp_insert_heading', '제목 텍스트를 삽입합니다 (H1~H6). 공문서 순번 체계의 대제목 등에 사용. numbering으로 자동 순번을 붙일 수 있습니다 (예: Ⅰ. 제목, 1. 제목, 가. 제목).', {
477
+ text: z.string().describe('제목 텍스트'),
478
+ level: z.number().int().min(1).max(6).describe('제목 레벨 (1=가장 큰 22pt, 6=가장 작은 10pt)'),
479
+ numbering: z.enum(['roman', 'decimal', 'korean', 'circle', 'paren_decimal', 'paren_korean']).optional().describe('순번 형식: roman(Ⅰ,Ⅱ), decimal(1,2), korean(가,나), circle(①,②), paren_decimal(1),2)), paren_korean(가),나))'),
480
+ number: z.number().int().min(1).max(10).optional().describe('순번 번호 (1~10, 기본 1)'),
481
+ }, async ({ text, level, numbering, number }) => {
482
+ if (!bridge.getCurrentDocument())
483
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
484
+ try {
485
+ await bridge.ensureRunning();
486
+ const params = { text, level };
487
+ if (numbering)
488
+ params.numbering = numbering;
489
+ if (number)
490
+ params.number = number;
491
+ const r = await bridge.send('insert_heading', params, FILL_TIMEOUT);
492
+ if (!r.success)
493
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
494
+ bridge.setCachedAnalysis(null);
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
+ server.tool('hwp_export_docx', '현재 문서를 DOCX(Word) 형식으로 내보냅니다.', { output_path: z.string().describe('DOCX 저장 경로') }, async ({ output_path }) => {
502
+ if (!bridge.getCurrentDocument())
503
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
504
+ try {
505
+ await bridge.ensureRunning();
506
+ const resolved = path.resolve(output_path);
507
+ const r = await bridge.send('export_format', { path: resolved, format: 'OOXML' }, 120000);
508
+ if (!r.success)
509
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
510
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
511
+ }
512
+ catch (err) {
513
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
514
+ }
515
+ });
516
+ server.tool('hwp_export_html', '현재 문서를 HTML 형식으로 내보냅니다.', { output_path: z.string().describe('HTML 저장 경로') }, async ({ output_path }) => {
517
+ if (!bridge.getCurrentDocument())
518
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
519
+ try {
520
+ await bridge.ensureRunning();
521
+ const resolved = path.resolve(output_path);
522
+ const r = await bridge.send('export_format', { path: resolved, format: 'HTML' }, 60000);
523
+ if (!r.success)
524
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
525
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
526
+ }
527
+ catch (err) {
528
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
529
+ }
530
+ });
531
+ server.tool('hwp_set_background_picture', '문서에 배경 이미지를 설정합니다. 워터마크로 활용 가능합니다.', { file_path: z.string().describe('배경 이미지 파일 경로') }, async ({ file_path }) => {
532
+ if (!bridge.getCurrentDocument())
533
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
534
+ try {
535
+ await bridge.ensureRunning();
536
+ const resolved = path.resolve(file_path);
537
+ const r = await bridge.send('set_background_picture', { file_path: resolved }, FILL_TIMEOUT);
538
+ if (!r.success)
539
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
540
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
541
+ }
542
+ catch (err) {
543
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
544
+ }
545
+ });
546
+ server.tool('hwp_table_split_cell', '표에서 현재 셀을 분할합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
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('table_split_cell', { table_index }, FILL_TIMEOUT);
552
+ if (!r.success)
553
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
554
+ bridge.setCachedAnalysis(null);
555
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
556
+ }
557
+ catch (err) {
558
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
559
+ }
560
+ });
561
+ server.tool('hwp_insert_hyperlink', '현재 커서 위치에 하이퍼링크를 삽입합니다.', {
562
+ url: z.string().describe('URL (예: https://example.com)'),
563
+ text: z.string().optional().describe('표시 텍스트 (생략 시 URL)'),
564
+ }, async ({ url, text }) => {
565
+ if (!bridge.getCurrentDocument())
566
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
567
+ try {
568
+ await bridge.ensureRunning();
569
+ const params = { url };
570
+ if (text)
571
+ params.text = text;
572
+ const r = await bridge.send('insert_hyperlink', params, FILL_TIMEOUT);
573
+ if (!r.success)
574
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
575
+ bridge.setCachedAnalysis(null);
576
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
577
+ }
578
+ catch (err) {
579
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
580
+ }
581
+ });
582
+ server.tool('hwp_insert_footnote', '현재 커서 위치에 각주를 삽입합니다. 학술 문서나 보고서에서 참조 주석을 달 때 사용하세요.', { text: z.string().optional().describe('각주 내용 (생략 시 빈 각주)') }, async ({ text }) => {
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('insert_footnote', text ? { text } : {}, FILL_TIMEOUT);
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
+ server.tool('hwp_insert_endnote', '현재 커서 위치에 미주를 삽입합니다.', { text: z.string().optional().describe('미주 내용') }, async ({ text }) => {
597
+ if (!bridge.getCurrentDocument())
598
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
599
+ try {
600
+ await bridge.ensureRunning();
601
+ const r = await bridge.send('insert_endnote', text ? { text } : {}, FILL_TIMEOUT);
602
+ if (!r.success)
603
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
604
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
605
+ }
606
+ catch (err) {
607
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
608
+ }
609
+ });
610
+ server.tool('hwp_insert_page_num', '현재 커서 위치에 쪽 번호를 삽입합니다. format으로 형식을 지정할 수 있습니다 (예: - 1 -, (1)).', {
611
+ format: z.enum(['plain', 'dash', 'paren']).optional().describe('페이지 번호 형식: plain(기본), dash(- 1 -), paren((1))'),
612
+ }, async ({ format }) => {
613
+ if (!bridge.getCurrentDocument())
614
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
615
+ try {
616
+ await bridge.ensureRunning();
617
+ const params = {};
618
+ if (format)
619
+ params.format = format;
620
+ const r = await bridge.send('insert_page_num', params);
621
+ if (!r.success)
622
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
623
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
624
+ }
625
+ catch (err) {
626
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
627
+ }
628
+ });
629
+ server.tool('hwp_insert_date_code', '현재 커서 위치에 오늘 날짜를 자동 삽입합니다.', {}, async () => {
630
+ if (!bridge.getCurrentDocument())
631
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
632
+ try {
633
+ await bridge.ensureRunning();
634
+ const r = await bridge.send('insert_date_code', {});
635
+ if (!r.success)
636
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
637
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
638
+ }
639
+ catch (err) {
640
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
641
+ }
642
+ });
643
+ server.tool('hwp_table_formula_sum', '표에서 합계를 자동 계산합니다. 현재 셀 위치에서 한글의 자동 합계 기능을 실행합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
644
+ if (!bridge.getCurrentDocument())
645
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
646
+ try {
647
+ await bridge.ensureRunning();
648
+ const r = await bridge.send('table_formula_sum', { table_index }, FILL_TIMEOUT);
649
+ if (!r.success)
650
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
651
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
652
+ }
653
+ catch (err) {
654
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
655
+ }
656
+ });
657
+ server.tool('hwp_table_formula_avg', '표에서 평균을 자동 계산합니다. 현재 셀 위치에서 한글의 자동 평균 기능을 실행합니다.', { table_index: z.number().int().min(0).describe('표 인덱스') }, async ({ table_index }) => {
658
+ if (!bridge.getCurrentDocument())
659
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
660
+ try {
661
+ await bridge.ensureRunning();
662
+ const r = await bridge.send('table_formula_avg', { table_index }, FILL_TIMEOUT);
663
+ if (!r.success)
664
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
665
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
666
+ }
667
+ catch (err) {
668
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
669
+ }
670
+ });
671
+ // ── Phase B: Quick Win 8개 ──
672
+ server.tool('hwp_table_to_csv', '표 데이터를 CSV 파일로 내보냅니다.', {
673
+ table_index: z.number().int().min(0).describe('표 인덱스'),
674
+ output_path: z.string().describe('CSV 저장 경로'),
675
+ }, async ({ table_index, output_path }) => {
676
+ if (!bridge.getCurrentDocument())
677
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
678
+ try {
679
+ await bridge.ensureRunning();
680
+ const r = await bridge.send('table_to_csv', { table_index, output_path: path.resolve(output_path) }, FILL_TIMEOUT);
681
+ if (!r.success)
682
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
683
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
684
+ }
685
+ catch (err) {
686
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
687
+ }
688
+ });
689
+ server.tool('hwp_break_section', '현재 위치에 섹션 나누기를 삽입합니다.', {}, async () => {
690
+ if (!bridge.getCurrentDocument())
691
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
692
+ try {
693
+ await bridge.ensureRunning();
694
+ const r = await bridge.send('break_section', {});
695
+ if (!r.success)
696
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
697
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
698
+ }
699
+ catch (err) {
700
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
701
+ }
702
+ });
703
+ server.tool('hwp_break_column', '현재 위치에 다단 나누기를 삽입합니다.', {}, async () => {
704
+ if (!bridge.getCurrentDocument())
705
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
706
+ try {
707
+ await bridge.ensureRunning();
708
+ const r = await bridge.send('break_column', {});
709
+ if (!r.success)
710
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
711
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
712
+ }
713
+ catch (err) {
714
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
715
+ }
716
+ });
717
+ server.tool('hwp_insert_line', '현재 위치에 선(줄)을 삽입합니다.', {}, async () => {
718
+ if (!bridge.getCurrentDocument())
719
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
720
+ try {
721
+ await bridge.ensureRunning();
722
+ const r = await bridge.send('insert_line', {});
723
+ if (!r.success)
724
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
725
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
726
+ }
727
+ catch (err) {
728
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
729
+ }
730
+ });
731
+ server.tool('hwp_table_swap_type', '표의 행과 열을 교환합니다.', {
732
+ table_index: z.number().int().min(0).describe('표 인덱스'),
733
+ }, async ({ table_index }) => {
734
+ if (!bridge.getCurrentDocument())
735
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
736
+ try {
737
+ await bridge.ensureRunning();
738
+ const r = await bridge.send('table_swap_type', { table_index }, FILL_TIMEOUT);
739
+ if (!r.success)
740
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
741
+ bridge.setCachedAnalysis(null);
742
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
743
+ }
744
+ catch (err) {
745
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
746
+ }
747
+ });
748
+ server.tool('hwp_insert_auto_num', '자동 번호매기기를 삽입합니다.', {}, async () => {
749
+ if (!bridge.getCurrentDocument())
750
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
751
+ try {
752
+ await bridge.ensureRunning();
753
+ const r = await bridge.send('insert_auto_num', {});
754
+ if (!r.success)
755
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
756
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
757
+ }
758
+ catch (err) {
759
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
760
+ }
761
+ });
762
+ server.tool('hwp_insert_memo', '메모 필드를 삽입합니다.', {
763
+ text: z.string().optional().describe('메모 내용'),
764
+ }, async ({ text }) => {
765
+ if (!bridge.getCurrentDocument())
766
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
767
+ try {
768
+ await bridge.ensureRunning();
769
+ const r = await bridge.send('insert_memo', text ? { text } : {}, FILL_TIMEOUT);
770
+ if (!r.success)
771
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
772
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
773
+ }
774
+ catch (err) {
775
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
776
+ }
777
+ });
778
+ server.tool('hwp_table_distribute_width', '표 셀 너비를 균등하게 분배합니다.', {
779
+ table_index: z.number().int().min(0).describe('표 인덱스'),
780
+ }, async ({ table_index }) => {
781
+ if (!bridge.getCurrentDocument())
782
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
783
+ try {
784
+ await bridge.ensureRunning();
785
+ const r = await bridge.send('table_distribute_width', { table_index }, FILL_TIMEOUT);
786
+ if (!r.success)
787
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
788
+ bridge.setCachedAnalysis(null);
789
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
790
+ }
791
+ catch (err) {
792
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
793
+ }
794
+ });
795
+ server.tool('hwp_indent', '현재 커서 위치의 단락을 들여쓰기합니다 (Shift+Tab 효과). 공문서 순번 체계에서 하위 항목 들여쓰기에 사용.', {
796
+ depth: z.number().optional().describe('들여쓰기 깊이 (pt, 기본 10)'),
797
+ }, async ({ depth }) => {
798
+ if (!bridge.getCurrentDocument())
799
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
800
+ try {
801
+ await bridge.ensureRunning();
802
+ const r = await bridge.send('indent', depth ? { depth } : {}, FILL_TIMEOUT);
803
+ if (!r.success)
804
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
805
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
806
+ }
807
+ catch (err) {
808
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
809
+ }
810
+ });
811
+ server.tool('hwp_outdent', '현재 커서 위치의 단락 들여쓰기를 줄입니다 (내어쓰기).', {
812
+ depth: z.number().optional().describe('내어쓰기 깊이 (pt, 기본 10)'),
813
+ }, async ({ depth }) => {
814
+ if (!bridge.getCurrentDocument())
815
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
816
+ try {
817
+ await bridge.ensureRunning();
818
+ const r = await bridge.send('outdent', depth ? { depth } : {}, FILL_TIMEOUT);
819
+ if (!r.success)
820
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
821
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
822
+ }
823
+ catch (err) {
824
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
825
+ }
826
+ });
827
+ server.tool('hwp_delete_guide_text', '양식의 작성요령/가이드 텍스트(< 작성요령 >, ※ 안내문 등)를 자동 삭제합니다. 공공기관 양식 작성 후 제출 전에 사용.', {
828
+ patterns: z.array(z.string()).optional().describe('삭제할 텍스트 패턴 목록 (기본: < 작성요령 >)'),
829
+ }, async ({ patterns }) => {
830
+ if (!bridge.getCurrentDocument())
831
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
832
+ try {
833
+ await bridge.ensureRunning();
834
+ const params = {};
835
+ if (patterns)
836
+ params.patterns = patterns;
837
+ const r = await bridge.send('delete_guide_text', params, FILL_TIMEOUT);
838
+ if (!r.success)
839
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
840
+ bridge.setCachedAnalysis(null);
841
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
842
+ }
843
+ catch (err) {
844
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
845
+ }
846
+ });
847
+ server.tool('hwp_toggle_checkbox', '문서의 체크박스를 전환합니다 (□→■, ☐→☑ 등). 양식에서 특정 항목을 체크할 때 사용.', {
848
+ find: z.string().describe('찾을 체크박스 텍스트 (예: "☐ 유")'),
849
+ replace: z.string().describe('바꿀 텍스트 (예: "■ 유")'),
850
+ }, async ({ find, replace }) => {
851
+ if (!bridge.getCurrentDocument())
852
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
853
+ try {
854
+ await bridge.ensureRunning();
855
+ const r = await bridge.send('toggle_checkbox', { find, replace }, FILL_TIMEOUT);
856
+ if (!r.success)
857
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
858
+ bridge.setCachedAnalysis(null);
859
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
860
+ }
861
+ catch (err) {
862
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
863
+ }
864
+ });
865
+ // ── 표 셀 배경색 ──
866
+ server.tool('hwp_set_cell_color', '표 셀의 배경색을 설정합니다. 간트차트 음영, 헤더행 강조, 데이터 시각화 등에 사용.', {
867
+ table_index: z.number().int().describe('표 인덱스 (0부터, -1=현재 위치한 표)'),
868
+ cells: z.array(z.object({
869
+ tab: z.number().int().describe('셀 탭 인덱스'),
870
+ color: z.string().describe('배경색 (#RRGGBB 형식, 예: "#E8E8E8")'),
871
+ })).describe('배경색을 설정할 셀 목록'),
872
+ }, async ({ table_index, cells }) => {
873
+ if (!bridge.getCurrentDocument())
874
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
875
+ try {
876
+ await bridge.ensureRunning();
877
+ const r = await bridge.send('set_cell_color', { table_index, cells }, FILL_TIMEOUT);
878
+ if (!r.success)
879
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
880
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
881
+ }
882
+ catch (err) {
883
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
884
+ }
885
+ });
886
+ // ── 표 테두리 스타일 ──
887
+ server.tool('hwp_set_table_border', '표의 테두리 스타일을 설정합니다. 표 전체 또는 특정 셀의 테두리를 변경할 수 있습니다.', {
888
+ table_index: z.number().int().describe('표 인덱스 (0부터)'),
889
+ cells: z.array(z.object({
890
+ tab: z.number().int().describe('셀 탭 인덱스'),
891
+ })).optional().describe('특정 셀만 적용 (생략 시 표 전체)'),
892
+ style: z.object({
893
+ line_type: z.number().int().min(0).max(5).optional().describe('선 종류: 0=없음, 1=실선, 2=파선, 3=점선, 4=1점쇄선, 5=2점쇄선'),
894
+ line_width: z.number().optional().describe('선 두께 (pt 단위)'),
895
+ }).optional().describe('테두리 스타일'),
896
+ }, async ({ table_index, cells, style }) => {
897
+ if (!bridge.getCurrentDocument())
898
+ return { content: [{ type: 'text', text: JSON.stringify({ error: '열린 문서가 없습니다.' }) }], isError: true };
899
+ try {
900
+ await bridge.ensureRunning();
901
+ const params = { table_index };
902
+ if (cells)
903
+ params.cells = cells;
904
+ if (style)
905
+ params.style = style;
906
+ const r = await bridge.send('set_table_border', params, FILL_TIMEOUT);
907
+ if (!r.success)
908
+ return { content: [{ type: 'text', text: JSON.stringify({ error: r.error }) }], isError: true };
909
+ return { content: [{ type: 'text', text: JSON.stringify(r.data) }] };
910
+ }
911
+ catch (err) {
912
+ return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }], isError: true };
913
+ }
914
+ });
915
+ } // end standard+ tools
916
+ }