@kimdayoun/hwpx-mcp 0.3.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.
package/dist/index.js ADDED
@@ -0,0 +1,4317 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
38
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
39
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const HwpxDocument_1 = require("./HwpxDocument");
43
+ const HangingIndentCalculator_1 = require("./HangingIndentCalculator");
44
+ // Version marker for debugging
45
+ const MCP_VERSION = 'v2-fixed-xml-replacement';
46
+ console.error(`[HWPX MCP] Server starting - ${MCP_VERSION} - ${new Date().toISOString()}`);
47
+ // Document storage
48
+ const openDocuments = new Map();
49
+ // Document-level locks to prevent race conditions during parallel updates
50
+ // Each document has a promise chain that serializes operations
51
+ const documentLocks = new Map();
52
+ /**
53
+ * Acquire a lock for a document operation.
54
+ * All operations on the same document will be serialized.
55
+ */
56
+ async function withDocumentLock(docId, operation) {
57
+ // Get the current lock promise (or resolved if none)
58
+ const currentLock = documentLocks.get(docId) || Promise.resolve();
59
+ // Create a new promise that will resolve when our operation completes
60
+ let releaseLock;
61
+ const newLock = new Promise((resolve) => {
62
+ releaseLock = resolve;
63
+ });
64
+ // Set our lock as the new pending lock
65
+ documentLocks.set(docId, newLock);
66
+ try {
67
+ // Wait for any previous operation to complete
68
+ await currentLock;
69
+ // Execute our operation
70
+ return await operation();
71
+ }
72
+ finally {
73
+ // Release the lock
74
+ releaseLock();
75
+ // Clean up if this is the last lock
76
+ if (documentLocks.get(docId) === newLock) {
77
+ documentLocks.delete(docId);
78
+ }
79
+ }
80
+ }
81
+ function generateId() {
82
+ return Math.random().toString(36).substring(2, 11);
83
+ }
84
+ // ============================================================
85
+ // Tool Definitions
86
+ // ============================================================
87
+ const tools = [
88
+ // === 🎯 Tool Guide (START HERE) ===
89
+ {
90
+ name: 'get_tool_guide',
91
+ description: `🎯 START HERE! Get recommended tools for your task. Call this FIRST to understand which tools to use.
92
+
93
+ Available workflows:
94
+ - "template": Fill content into existing template/form (preserving styles)
95
+ - "table": Work with tables (find, read, modify)
96
+ - "image": Insert images or diagrams
97
+ - "search": Find and replace text
98
+ - "read": Read and analyze document content
99
+ - "create": Create new document from scratch
100
+ - "all": Get complete tool reference
101
+
102
+ Example: get_tool_guide({ workflow: "template" })`,
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ workflow: {
107
+ type: 'string',
108
+ description: 'Workflow type: template, table, image, search, read, create, or all',
109
+ enum: ['template', 'table', 'image', 'search', 'read', 'create', 'all']
110
+ },
111
+ },
112
+ required: ['workflow'],
113
+ },
114
+ },
115
+ // === Document Management ===
116
+ {
117
+ name: 'open_document',
118
+ description: 'Open an HWPX or HWP document for reading and editing',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ file_path: { type: 'string', description: 'Path to the HWPX or HWP file' },
123
+ },
124
+ required: ['file_path'],
125
+ },
126
+ },
127
+ {
128
+ name: 'close_document',
129
+ description: 'Close an open document',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ doc_id: { type: 'string', description: 'Document ID from open_document' },
134
+ },
135
+ required: ['doc_id'],
136
+ },
137
+ },
138
+ {
139
+ name: 'save_document',
140
+ description: 'Save the document (HWPX only). Supports backup creation and integrity verification.',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ doc_id: { type: 'string', description: 'Document ID' },
145
+ output_path: { type: 'string', description: 'Output path (optional, saves to original if omitted)' },
146
+ create_backup: { type: 'boolean', description: 'Create .bak backup before saving (default: true)' },
147
+ verify_integrity: { type: 'boolean', description: 'Verify saved file integrity (default: true)' },
148
+ },
149
+ required: ['doc_id'],
150
+ },
151
+ },
152
+ {
153
+ name: 'list_open_documents',
154
+ description: 'List all currently open documents',
155
+ inputSchema: { type: 'object', properties: {} },
156
+ },
157
+ // === Document Info ===
158
+ {
159
+ name: 'get_document_text',
160
+ description: 'Get all text content from the document',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ doc_id: { type: 'string', description: 'Document ID' },
165
+ },
166
+ required: ['doc_id'],
167
+ },
168
+ },
169
+ {
170
+ name: 'get_document_structure',
171
+ description: 'Get document structure (sections, paragraphs, tables, images count)',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ doc_id: { type: 'string', description: 'Document ID' },
176
+ },
177
+ required: ['doc_id'],
178
+ },
179
+ },
180
+ {
181
+ name: 'get_document_metadata',
182
+ description: 'Get document metadata (title, author, dates, etc.)',
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: {
186
+ doc_id: { type: 'string', description: 'Document ID' },
187
+ },
188
+ required: ['doc_id'],
189
+ },
190
+ },
191
+ {
192
+ name: 'set_document_metadata',
193
+ description: 'Set document metadata (HWPX only)',
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ doc_id: { type: 'string', description: 'Document ID' },
198
+ title: { type: 'string', description: 'Document title' },
199
+ creator: { type: 'string', description: 'Author name' },
200
+ subject: { type: 'string', description: 'Subject' },
201
+ description: { type: 'string', description: 'Description' },
202
+ },
203
+ required: ['doc_id'],
204
+ },
205
+ },
206
+ // === Paragraph Operations ===
207
+ {
208
+ name: 'get_paragraphs',
209
+ description: 'Get paragraphs from the document with their text and styles',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ doc_id: { type: 'string', description: 'Document ID' },
214
+ section_index: { type: 'number', description: 'Section index (optional, all if omitted)' },
215
+ },
216
+ required: ['doc_id'],
217
+ },
218
+ },
219
+ {
220
+ name: 'get_paragraph',
221
+ description: 'Get a specific paragraph with full details',
222
+ inputSchema: {
223
+ type: 'object',
224
+ properties: {
225
+ doc_id: { type: 'string', description: 'Document ID' },
226
+ section_index: { type: 'number', description: 'Section index' },
227
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
228
+ },
229
+ required: ['doc_id', 'section_index', 'paragraph_index'],
230
+ },
231
+ },
232
+ {
233
+ name: 'insert_paragraph',
234
+ description: 'Insert a new paragraph (HWPX only). Automatically applies hanging indent if text contains a marker like "○ ", "1. ", "가. ", etc.',
235
+ inputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ doc_id: { type: 'string', description: 'Document ID' },
239
+ section_index: { type: 'number', description: 'Section index' },
240
+ after_index: { type: 'number', description: 'Insert after this paragraph index (-1 for beginning)' },
241
+ text: { type: 'string', description: 'Paragraph text' },
242
+ auto_hanging_indent: { type: 'boolean', description: 'Automatically apply hanging indent if marker detected (default: true)' },
243
+ },
244
+ required: ['doc_id', 'section_index', 'after_index', 'text'],
245
+ },
246
+ },
247
+ {
248
+ name: 'delete_paragraph',
249
+ description: 'Delete a paragraph (HWPX only)',
250
+ inputSchema: {
251
+ type: 'object',
252
+ properties: {
253
+ doc_id: { type: 'string', description: 'Document ID' },
254
+ section_index: { type: 'number', description: 'Section index' },
255
+ paragraph_index: { type: 'number', description: 'Paragraph index to delete' },
256
+ },
257
+ required: ['doc_id', 'section_index', 'paragraph_index'],
258
+ },
259
+ },
260
+ {
261
+ name: 'update_paragraph_text',
262
+ description: `⭐ RECOMMENDED for template work. Update paragraph text content while PRESERVING existing styles (font, alignment, size).
263
+
264
+ When working with templates/forms:
265
+ - Use this tool to change text content only
266
+ - Original paraPrIDRef (paragraph style) is kept intact
267
+ - Existing formatting (alignment, font, size) remains unchanged
268
+
269
+ Example workflow for templates:
270
+ 1. Open template file with pre-set styles
271
+ 2. Use update_paragraph_text to fill in content
272
+ 3. Save - all original formatting preserved
273
+
274
+ ⚠️ If you need to CHANGE alignment/style, use set_paragraph_style instead.
275
+ ⚠️ For paragraphs with multiple styled runs (bold + normal), use update_paragraph_text_preserve_styles.`,
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: {
279
+ doc_id: { type: 'string', description: 'Document ID' },
280
+ section_index: { type: 'number', description: 'Section index' },
281
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
282
+ run_index: { type: 'number', description: 'Run index (default 0)' },
283
+ text: { type: 'string', description: 'New text content' },
284
+ },
285
+ required: ['doc_id', 'section_index', 'paragraph_index', 'text'],
286
+ },
287
+ },
288
+ {
289
+ name: 'update_paragraph_text_preserve_styles',
290
+ description: `Update paragraph text while preserving the style structure of multiple runs.
291
+
292
+ When a paragraph has multiple styled runs (e.g., bold + normal text), this tool distributes
293
+ the new text across runs proportionally while keeping their original character styles.
294
+
295
+ Strategy:
296
+ - Distributes new text proportionally based on original run lengths
297
+ - Preserves charPrIDRef (character style) of each run
298
+ - If new text is longer, extends the last run
299
+ - If original has no text, sets to first run
300
+
301
+ Use this instead of update_paragraph_text when you need to maintain style formatting
302
+ across multiple runs within a single paragraph.
303
+
304
+ Example: Paragraph with "Hello" (bold) + " World" (normal)
305
+ → update_paragraph_text_preserve_styles("Goodbye Universe")
306
+ → Result: "Goodbye" (bold) + " Universe" (normal)`,
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ doc_id: { type: 'string', description: 'Document ID' },
311
+ section_index: { type: 'number', description: 'Section index' },
312
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
313
+ text: { type: 'string', description: 'New text content' },
314
+ },
315
+ required: ['doc_id', 'section_index', 'paragraph_index', 'text'],
316
+ },
317
+ },
318
+ {
319
+ name: 'append_text_to_paragraph',
320
+ description: 'Append text to an existing paragraph (HWPX only)',
321
+ inputSchema: {
322
+ type: 'object',
323
+ properties: {
324
+ doc_id: { type: 'string', description: 'Document ID' },
325
+ section_index: { type: 'number', description: 'Section index' },
326
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
327
+ text: { type: 'string', description: 'Text to append' },
328
+ },
329
+ required: ['doc_id', 'section_index', 'paragraph_index', 'text'],
330
+ },
331
+ },
332
+ // === Character Styling ===
333
+ {
334
+ name: 'set_text_style',
335
+ description: 'Apply character formatting to a paragraph run (HWPX only)',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ doc_id: { type: 'string', description: 'Document ID' },
340
+ section_index: { type: 'number', description: 'Section index' },
341
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
342
+ run_index: { type: 'number', description: 'Run index (default 0)' },
343
+ bold: { type: 'boolean', description: 'Bold' },
344
+ italic: { type: 'boolean', description: 'Italic' },
345
+ underline: { type: 'boolean', description: 'Underline' },
346
+ strikethrough: { type: 'boolean', description: 'Strikethrough' },
347
+ font_name: { type: 'string', description: 'Font name' },
348
+ font_size: { type: 'number', description: 'Font size in pt' },
349
+ font_color: { type: 'string', description: 'Text color (hex)' },
350
+ background_color: { type: 'string', description: 'Background color (hex)' },
351
+ },
352
+ required: ['doc_id', 'section_index', 'paragraph_index'],
353
+ },
354
+ },
355
+ {
356
+ name: 'get_text_style',
357
+ description: 'Get character formatting of a paragraph',
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ doc_id: { type: 'string', description: 'Document ID' },
362
+ section_index: { type: 'number', description: 'Section index' },
363
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
364
+ run_index: { type: 'number', description: 'Run index (optional)' },
365
+ },
366
+ required: ['doc_id', 'section_index', 'paragraph_index'],
367
+ },
368
+ },
369
+ // === Paragraph Styling ===
370
+ {
371
+ name: 'set_paragraph_style',
372
+ description: 'Apply paragraph formatting (HWPX only)',
373
+ inputSchema: {
374
+ type: 'object',
375
+ properties: {
376
+ doc_id: { type: 'string', description: 'Document ID' },
377
+ section_index: { type: 'number', description: 'Section index' },
378
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
379
+ align: { type: 'string', enum: ['left', 'center', 'right', 'justify', 'distribute'], description: 'Text alignment' },
380
+ line_spacing: { type: 'number', description: 'Line spacing in %' },
381
+ margin_left: { type: 'number', description: 'Left margin in pt' },
382
+ margin_right: { type: 'number', description: 'Right margin in pt' },
383
+ margin_top: { type: 'number', description: 'Top margin in pt' },
384
+ margin_bottom: { type: 'number', description: 'Bottom margin in pt' },
385
+ first_line_indent: { type: 'number', description: 'First line indent in pt' },
386
+ },
387
+ required: ['doc_id', 'section_index', 'paragraph_index'],
388
+ },
389
+ },
390
+ {
391
+ name: 'get_paragraph_style',
392
+ description: 'Get paragraph formatting',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ doc_id: { type: 'string', description: 'Document ID' },
397
+ section_index: { type: 'number', description: 'Section index' },
398
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
399
+ },
400
+ required: ['doc_id', 'section_index', 'paragraph_index'],
401
+ },
402
+ },
403
+ // === Hanging Indent (내어쓰기) ===
404
+ {
405
+ name: 'set_hanging_indent',
406
+ description: `Set hanging indent with MANUAL pt value (HWPX only).
407
+
408
+ 💡 In most cases, use set_auto_hanging_indent instead - it automatically detects markers and calculates the correct indent.
409
+
410
+ Use this manual version only when:
411
+ - You need a specific indent value (e.g., exactly 20pt)
412
+ - Auto-detection doesn't work for your marker type
413
+ - You want custom indentation regardless of marker`,
414
+ inputSchema: {
415
+ type: 'object',
416
+ properties: {
417
+ doc_id: { type: 'string', description: 'Document ID' },
418
+ section_index: { type: 'number', description: 'Section index' },
419
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
420
+ indent_pt: { type: 'number', description: 'Indent amount in points (positive value)' },
421
+ },
422
+ required: ['doc_id', 'section_index', 'paragraph_index', 'indent_pt'],
423
+ },
424
+ },
425
+ {
426
+ name: 'get_hanging_indent',
427
+ description: 'Get hanging indent value for a paragraph',
428
+ inputSchema: {
429
+ type: 'object',
430
+ properties: {
431
+ doc_id: { type: 'string', description: 'Document ID' },
432
+ section_index: { type: 'number', description: 'Section index' },
433
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
434
+ },
435
+ required: ['doc_id', 'section_index', 'paragraph_index'],
436
+ },
437
+ },
438
+ {
439
+ name: 'remove_hanging_indent',
440
+ description: 'Remove hanging indent from a paragraph (HWPX only)',
441
+ inputSchema: {
442
+ type: 'object',
443
+ properties: {
444
+ doc_id: { type: 'string', description: 'Document ID' },
445
+ section_index: { type: 'number', description: 'Section index' },
446
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
447
+ },
448
+ required: ['doc_id', 'section_index', 'paragraph_index'],
449
+ },
450
+ },
451
+ {
452
+ name: 'set_table_cell_hanging_indent',
453
+ description: 'Set hanging indent on a paragraph inside a table cell (HWPX only). Hanging indent pulls the first line left while indenting the rest of the lines.',
454
+ inputSchema: {
455
+ type: 'object',
456
+ properties: {
457
+ doc_id: { type: 'string', description: 'Document ID' },
458
+ section_index: { type: 'number', description: 'Section index' },
459
+ table_index: { type: 'number', description: 'Table index within section' },
460
+ row: { type: 'number', description: 'Row index (0-based)' },
461
+ col: { type: 'number', description: 'Column index (0-based)' },
462
+ paragraph_index: { type: 'number', description: 'Paragraph index within cell (0-based)' },
463
+ indent_pt: { type: 'number', description: 'Indent amount in points (positive value)' },
464
+ },
465
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'paragraph_index', 'indent_pt'],
466
+ },
467
+ },
468
+ {
469
+ name: 'get_table_cell_hanging_indent',
470
+ description: 'Get hanging indent value for a paragraph inside a table cell',
471
+ inputSchema: {
472
+ type: 'object',
473
+ properties: {
474
+ doc_id: { type: 'string', description: 'Document ID' },
475
+ section_index: { type: 'number', description: 'Section index' },
476
+ table_index: { type: 'number', description: 'Table index within section' },
477
+ row: { type: 'number', description: 'Row index (0-based)' },
478
+ col: { type: 'number', description: 'Column index (0-based)' },
479
+ paragraph_index: { type: 'number', description: 'Paragraph index within cell (0-based)' },
480
+ },
481
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'paragraph_index'],
482
+ },
483
+ },
484
+ {
485
+ name: 'remove_table_cell_hanging_indent',
486
+ description: 'Remove hanging indent from a paragraph inside a table cell (HWPX only)',
487
+ inputSchema: {
488
+ type: 'object',
489
+ properties: {
490
+ doc_id: { type: 'string', description: 'Document ID' },
491
+ section_index: { type: 'number', description: 'Section index' },
492
+ table_index: { type: 'number', description: 'Table index within section' },
493
+ row: { type: 'number', description: 'Row index (0-based)' },
494
+ col: { type: 'number', description: 'Column index (0-based)' },
495
+ paragraph_index: { type: 'number', description: 'Paragraph index within cell (0-based)' },
496
+ },
497
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'paragraph_index'],
498
+ },
499
+ },
500
+ {
501
+ name: 'set_auto_hanging_indent',
502
+ description: 'Automatically set hanging indent based on detected marker in paragraph text (HWPX only). Detects markers like "○ ", "1. ", "가. ", "(1) ", "① " etc. and calculates appropriate indent width. If font_size is not provided, reads the actual font size from the document.',
503
+ inputSchema: {
504
+ type: 'object',
505
+ properties: {
506
+ doc_id: { type: 'string', description: 'Document ID' },
507
+ section_index: { type: 'number', description: 'Section index' },
508
+ paragraph_index: { type: 'number', description: 'Paragraph element index' },
509
+ font_size: { type: 'number', description: 'Font size in pt. If not provided, reads from document (falls back to 10pt if not found)' },
510
+ },
511
+ required: ['doc_id', 'section_index', 'paragraph_index'],
512
+ },
513
+ },
514
+ {
515
+ name: 'set_table_cell_auto_hanging_indent',
516
+ description: 'Automatically set hanging indent on a paragraph inside a table cell based on detected marker (HWPX only). Detects markers like "○ ", "1. ", "가. ", "(1) ", "① " etc. If font_size is not provided, reads the actual font size from the document.',
517
+ inputSchema: {
518
+ type: 'object',
519
+ properties: {
520
+ doc_id: { type: 'string', description: 'Document ID' },
521
+ section_index: { type: 'number', description: 'Section index' },
522
+ table_index: { type: 'number', description: 'Table index within section' },
523
+ row: { type: 'number', description: 'Row index (0-based)' },
524
+ col: { type: 'number', description: 'Column index (0-based)' },
525
+ paragraph_index: { type: 'number', description: 'Paragraph index within cell (0-based)' },
526
+ font_size: { type: 'number', description: 'Font size in pt. If not provided, reads from document (falls back to 10pt if not found)' },
527
+ },
528
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'paragraph_index'],
529
+ },
530
+ },
531
+ // === Search & Replace ===
532
+ {
533
+ name: 'search_text',
534
+ description: 'Search for text in the document (includes table cells by default)',
535
+ inputSchema: {
536
+ type: 'object',
537
+ properties: {
538
+ doc_id: { type: 'string', description: 'Document ID' },
539
+ query: { type: 'string', description: 'Text to search for' },
540
+ case_sensitive: { type: 'boolean', description: 'Case sensitive search (default: false)' },
541
+ regex: { type: 'boolean', description: 'Use regular expression (default: false)' },
542
+ include_tables: { type: 'boolean', description: 'Include table cell text in search (default: true)' },
543
+ },
544
+ required: ['doc_id', 'query'],
545
+ },
546
+ },
547
+ {
548
+ name: 'replace_text',
549
+ description: `Find and replace text throughout the ENTIRE document (HWPX only).
550
+
551
+ ⚠️ This searches ALL paragraphs and table cells in the document.
552
+
553
+ When to use:
554
+ - Bulk replacement (e.g., change "2024" to "2025" everywhere)
555
+ - Fix typos across the document
556
+ - Replace placeholder text (e.g., "[NAME]" → "홍길동")
557
+
558
+ When NOT to use:
559
+ - Updating a specific paragraph → use update_paragraph_text
560
+ - Updating a specific table cell → use update_table_cell or replace_text_in_cell`,
561
+ inputSchema: {
562
+ type: 'object',
563
+ properties: {
564
+ doc_id: { type: 'string', description: 'Document ID' },
565
+ old_text: { type: 'string', description: 'Text to find' },
566
+ new_text: { type: 'string', description: 'Replacement text' },
567
+ case_sensitive: { type: 'boolean', description: 'Case sensitive (default: false)' },
568
+ regex: { type: 'boolean', description: 'Use regular expression (default: false)' },
569
+ replace_all: { type: 'boolean', description: 'Replace all occurrences (default: true)' },
570
+ },
571
+ required: ['doc_id', 'old_text', 'new_text'],
572
+ },
573
+ },
574
+ {
575
+ name: 'batch_replace',
576
+ description: 'Perform multiple text replacements at once (HWPX only)',
577
+ inputSchema: {
578
+ type: 'object',
579
+ properties: {
580
+ doc_id: { type: 'string', description: 'Document ID' },
581
+ replacements: {
582
+ type: 'array',
583
+ items: {
584
+ type: 'object',
585
+ properties: {
586
+ old_text: { type: 'string' },
587
+ new_text: { type: 'string' },
588
+ },
589
+ },
590
+ description: 'Array of {old_text, new_text} pairs',
591
+ },
592
+ },
593
+ required: ['doc_id', 'replacements'],
594
+ },
595
+ },
596
+ {
597
+ name: 'replace_text_in_cell',
598
+ description: 'Replace text within a specific table cell (HWPX only). More targeted than replace_text.',
599
+ inputSchema: {
600
+ type: 'object',
601
+ properties: {
602
+ doc_id: { type: 'string', description: 'Document ID' },
603
+ section_index: { type: 'number', description: 'Section index' },
604
+ table_index: { type: 'number', description: 'Table index within section' },
605
+ row: { type: 'number', description: 'Row index (0-based)' },
606
+ col: { type: 'number', description: 'Column index (0-based)' },
607
+ old_text: { type: 'string', description: 'Text to find' },
608
+ new_text: { type: 'string', description: 'Replacement text' },
609
+ case_sensitive: { type: 'boolean', description: 'Case sensitive (default: false)' },
610
+ regex: { type: 'boolean', description: 'Use regular expression (default: false)' },
611
+ replace_all: { type: 'boolean', description: 'Replace all occurrences (default: true)' },
612
+ },
613
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'old_text', 'new_text'],
614
+ },
615
+ },
616
+ // === Table Operations ===
617
+ {
618
+ name: 'get_tables',
619
+ description: 'Get all tables from the document',
620
+ inputSchema: {
621
+ type: 'object',
622
+ properties: {
623
+ doc_id: { type: 'string', description: 'Document ID' },
624
+ },
625
+ required: ['doc_id'],
626
+ },
627
+ },
628
+ {
629
+ name: 'get_table_map',
630
+ description: `⭐ RECOMMENDED for finding tables. Returns ALL tables with their headers and metadata.
631
+
632
+ Returns for each table:
633
+ - table_index: Global index (use this for other table operations)
634
+ - header: Text from the paragraph BEFORE the table (usually the table title)
635
+ - size: rows × cols
636
+ - is_empty: Whether table has content
637
+ - first_row_preview: Preview of first row data
638
+
639
+ Use this FIRST when working with tables, then use the table_index for:
640
+ - get_table, update_table_cell, insert_image_in_cell, etc.
641
+
642
+ Alternative tools:
643
+ - find_table_by_header: Search by header text
644
+ - get_tables: Raw table list without headers`,
645
+ inputSchema: {
646
+ type: 'object',
647
+ properties: {
648
+ doc_id: { type: 'string', description: 'Document ID' },
649
+ },
650
+ required: ['doc_id'],
651
+ },
652
+ },
653
+ {
654
+ name: 'find_empty_tables',
655
+ description: 'Find tables that are empty or contain only placeholder text (dashes, bullets, numbers only)',
656
+ inputSchema: {
657
+ type: 'object',
658
+ properties: {
659
+ doc_id: { type: 'string', description: 'Document ID' },
660
+ },
661
+ required: ['doc_id'],
662
+ },
663
+ },
664
+ {
665
+ name: 'get_tables_by_section',
666
+ description: 'Get all tables within a specific section',
667
+ inputSchema: {
668
+ type: 'object',
669
+ properties: {
670
+ doc_id: { type: 'string', description: 'Document ID' },
671
+ section_index: { type: 'number', description: 'Section index (0-based)' },
672
+ },
673
+ required: ['doc_id', 'section_index'],
674
+ },
675
+ },
676
+ {
677
+ name: 'find_table_by_header',
678
+ description: 'Find tables by their header text (partial match, case-insensitive)',
679
+ inputSchema: {
680
+ type: 'object',
681
+ properties: {
682
+ doc_id: { type: 'string', description: 'Document ID' },
683
+ search_text: { type: 'string', description: 'Text to search for in table headers' },
684
+ },
685
+ required: ['doc_id', 'search_text'],
686
+ },
687
+ },
688
+ {
689
+ name: 'get_tables_summary',
690
+ description: 'Get summary of multiple tables by index range. Returns compact info: header, size, empty status, and content preview.',
691
+ inputSchema: {
692
+ type: 'object',
693
+ properties: {
694
+ doc_id: { type: 'string', description: 'Document ID' },
695
+ start_index: { type: 'number', description: 'Start table index (0-based, default: 0)' },
696
+ end_index: { type: 'number', description: 'End table index (inclusive, default: last table)' },
697
+ },
698
+ required: ['doc_id'],
699
+ },
700
+ },
701
+ {
702
+ name: 'get_document_outline',
703
+ description: 'Get document outline - hierarchical structure showing sections, headings, and tables with their positions',
704
+ inputSchema: {
705
+ type: 'object',
706
+ properties: {
707
+ doc_id: { type: 'string', description: 'Document ID' },
708
+ },
709
+ required: ['doc_id'],
710
+ },
711
+ },
712
+ // === Position/Index Helper Tools ===
713
+ {
714
+ name: 'get_element_index_for_table',
715
+ description: 'Convert a global table index to element index in its section. Use this to find the right position for inserting content near a table.',
716
+ inputSchema: {
717
+ type: 'object',
718
+ properties: {
719
+ doc_id: { type: 'string', description: 'Document ID' },
720
+ table_index: { type: 'number', description: 'Global table index (0-based, from get_tables or get_table_map)' },
721
+ },
722
+ required: ['doc_id', 'table_index'],
723
+ },
724
+ },
725
+ {
726
+ name: 'find_paragraph_by_text',
727
+ description: 'Find paragraphs containing specific text. Returns element indices with surrounding context.',
728
+ inputSchema: {
729
+ type: 'object',
730
+ properties: {
731
+ doc_id: { type: 'string', description: 'Document ID' },
732
+ search_text: { type: 'string', description: 'Text to search for (partial match, case-insensitive)' },
733
+ section_index: { type: 'number', description: 'Optional: limit search to specific section' },
734
+ },
735
+ required: ['doc_id', 'search_text'],
736
+ },
737
+ },
738
+ {
739
+ name: 'get_insert_context',
740
+ description: 'Get context around an element index to verify insertion point. Shows elements before/after.',
741
+ inputSchema: {
742
+ type: 'object',
743
+ properties: {
744
+ doc_id: { type: 'string', description: 'Document ID' },
745
+ section_index: { type: 'number', description: 'Section index' },
746
+ element_index: { type: 'number', description: 'Element index to inspect' },
747
+ context_range: { type: 'number', description: 'Number of elements before/after to show (default: 2)' },
748
+ },
749
+ required: ['doc_id', 'section_index', 'element_index'],
750
+ },
751
+ },
752
+ {
753
+ name: 'find_insert_position_after_header',
754
+ description: `Find the right insertion position after text. Searches both independent paragraphs AND table cell contents by default.
755
+
756
+ IMPORTANT - Check 'found_in' in the result:
757
+ - If found_in='paragraph': Use insert_image with section_index and insert_after to insert AFTER the paragraph
758
+ - If found_in='table_cell': The text is INSIDE a table cell. Use insert_image_in_cell with table_info (table_index, row, col) to insert the image INSIDE that cell. Do NOT use insert_image as it will place the image OUTSIDE the table.`,
759
+ inputSchema: {
760
+ type: 'object',
761
+ properties: {
762
+ doc_id: { type: 'string', description: 'Document ID' },
763
+ header_text: { type: 'string', description: 'Header/title text to search for' },
764
+ search_in: {
765
+ type: 'string',
766
+ enum: ['paragraphs', 'table_cells', 'all'],
767
+ description: 'Where to search: "paragraphs" (independent paragraphs only), "table_cells" (table cell contents only), "all" (both, default). Many Korean documents have content inside table cells, so "all" is recommended.'
768
+ },
769
+ },
770
+ required: ['doc_id', 'header_text'],
771
+ },
772
+ },
773
+ {
774
+ name: 'find_insert_position_after_table',
775
+ description: `Find the right insertion position AFTER a specific table (OUTSIDE the table).
776
+
777
+ Returns section_index and insert_after value for use with insert_image/render_mermaid.
778
+ NOTE: This inserts AFTER the table, not inside it. To insert an image INSIDE a table cell, use insert_image_in_cell directly.`,
779
+ inputSchema: {
780
+ type: 'object',
781
+ properties: {
782
+ doc_id: { type: 'string', description: 'Document ID' },
783
+ table_index: { type: 'number', description: 'Global table index (0-based)' },
784
+ },
785
+ required: ['doc_id', 'table_index'],
786
+ },
787
+ },
788
+ {
789
+ name: 'get_table',
790
+ description: 'Get a specific table with full data',
791
+ inputSchema: {
792
+ type: 'object',
793
+ properties: {
794
+ doc_id: { type: 'string', description: 'Document ID' },
795
+ section_index: { type: 'number', description: 'Section index' },
796
+ table_index: { type: 'number', description: 'Table index within section' },
797
+ },
798
+ required: ['doc_id', 'section_index', 'table_index'],
799
+ },
800
+ },
801
+ {
802
+ name: 'get_table_cell',
803
+ description: 'Get content of a specific table cell',
804
+ inputSchema: {
805
+ type: 'object',
806
+ properties: {
807
+ doc_id: { type: 'string', description: 'Document ID' },
808
+ section_index: { type: 'number', description: 'Section index' },
809
+ table_index: { type: 'number', description: 'Table index' },
810
+ row: { type: 'number', description: 'Row index (0-based)' },
811
+ col: { type: 'number', description: 'Column index (0-based)' },
812
+ },
813
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col'],
814
+ },
815
+ },
816
+ {
817
+ name: 'update_table_cell',
818
+ description: `⭐ RECOMMENDED for template work with tables. Update cell content while PRESERVING existing styles.
819
+
820
+ Features:
821
+ - Preserves existing charPrIDRef (font styles) by default
822
+ - Automatically applies hanging indent if text contains markers (○, 1., 가., etc.)
823
+ - Resets lineseg for proper text layout
824
+
825
+ For template/form work:
826
+ - Use this to fill in table cell content
827
+ - Original cell formatting remains unchanged`,
828
+ inputSchema: {
829
+ type: 'object',
830
+ properties: {
831
+ doc_id: { type: 'string', description: 'Document ID' },
832
+ section_index: { type: 'number', description: 'Section index' },
833
+ table_index: { type: 'number', description: 'Table index' },
834
+ row: { type: 'number', description: 'Row index' },
835
+ col: { type: 'number', description: 'Column index' },
836
+ text: { type: 'string', description: 'New cell content' },
837
+ char_shape_id: { type: 'number', description: 'Character shape ID to apply (optional, uses existing style if omitted)' },
838
+ auto_hanging_indent: { type: 'boolean', description: 'Automatically apply hanging indent if marker detected (default: true)' },
839
+ },
840
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col', 'text'],
841
+ },
842
+ },
843
+ {
844
+ name: 'find_cell_by_label',
845
+ description: `🔍 Find table cells by label text and get the adjacent cell position.
846
+
847
+ Perfect for Korean documents with "레이블: 값" patterns.
848
+ Searches all tables for cells containing the label text.
849
+
850
+ Example: findCellByLabel("이름:") returns the cell to the right of "이름:" label.
851
+
852
+ Use case:
853
+ - Finding form fields by their labels
854
+ - Locating cells without knowing exact indices`,
855
+ inputSchema: {
856
+ type: 'object',
857
+ properties: {
858
+ doc_id: { type: 'string', description: 'Document ID' },
859
+ label_text: { type: 'string', description: 'Label text to search for (partial match, case-insensitive)' },
860
+ direction: { type: 'string', enum: ['right', 'down'], description: 'Direction from label to target cell (default: right)' },
861
+ },
862
+ required: ['doc_id', 'label_text'],
863
+ },
864
+ },
865
+ {
866
+ name: 'fill_by_path',
867
+ description: `⭐ RECOMMENDED for template work! Fill multiple cells using path-based addressing.
868
+
869
+ jkf87-style path format: "label > direction > direction"
870
+ - "이름: > right" → find "이름:" and fill the cell to its right
871
+ - "합계 > down > down" → find "합계" and fill 2 cells below
872
+ - Directions: right, left, up, down
873
+
874
+ Example:
875
+ fill_by_path({
876
+ mappings: {
877
+ "이름: > right": "홍길동",
878
+ "연락처: > right": "010-1234-5678",
879
+ "합계 > down": "1,000,000"
880
+ }
881
+ })
882
+
883
+ Much easier than specifying table_index, row, col manually!`,
884
+ inputSchema: {
885
+ type: 'object',
886
+ properties: {
887
+ doc_id: { type: 'string', description: 'Document ID' },
888
+ mappings: {
889
+ type: 'object',
890
+ description: 'Path-to-value mappings. Path format: "label > direction > ..." where direction is right/left/up/down',
891
+ additionalProperties: { type: 'string' }
892
+ },
893
+ },
894
+ required: ['doc_id', 'mappings'],
895
+ },
896
+ },
897
+ {
898
+ name: 'get_cell_context',
899
+ description: `Get surrounding cells' content around a specific cell.
900
+
901
+ Returns center cell and neighboring cells in each direction.
902
+ Useful for understanding table structure without loading entire table.
903
+
904
+ Example result:
905
+ {
906
+ "center": "현재 셀",
907
+ "up_1": "위 1칸",
908
+ "down_1": "아래 1칸",
909
+ "left_1": "왼쪽 1칸",
910
+ "right_1": "오른쪽 1칸"
911
+ }`,
912
+ inputSchema: {
913
+ type: 'object',
914
+ properties: {
915
+ doc_id: { type: 'string', description: 'Document ID' },
916
+ table_index: { type: 'number', description: 'Global table index (from get_table_map)' },
917
+ row: { type: 'number', description: 'Row index (0-based)' },
918
+ col: { type: 'number', description: 'Column index (0-based)' },
919
+ depth: { type: 'number', description: 'How many cells in each direction (default: 1)' },
920
+ },
921
+ required: ['doc_id', 'table_index', 'row', 'col'],
922
+ },
923
+ },
924
+ {
925
+ name: 'batch_fill_table',
926
+ description: `Fill multiple table cells at once from a 2D array.
927
+
928
+ Perfect for:
929
+ - Filling data tables from CSV/JSON
930
+ - Batch updating table content
931
+ - Template form filling
932
+
933
+ Example:
934
+ batch_fill_table({
935
+ data: [
936
+ ["이름", "나이", "주소"],
937
+ ["홍길동", "30", "서울"],
938
+ ["김철수", "25", "부산"]
939
+ ],
940
+ start_row: 0,
941
+ start_col: 0
942
+ })`,
943
+ inputSchema: {
944
+ type: 'object',
945
+ properties: {
946
+ doc_id: { type: 'string', description: 'Document ID' },
947
+ table_index: { type: 'number', description: 'Global table index (from get_table_map)' },
948
+ data: {
949
+ type: 'array',
950
+ items: { type: 'array', items: { type: 'string' } },
951
+ description: '2D array of cell values'
952
+ },
953
+ start_row: { type: 'number', description: 'Starting row index (default: 0)' },
954
+ start_col: { type: 'number', description: 'Starting column index (default: 0)' },
955
+ },
956
+ required: ['doc_id', 'table_index', 'data'],
957
+ },
958
+ },
959
+ {
960
+ name: 'set_cell_properties',
961
+ description: 'Set table cell properties (HWPX only)',
962
+ inputSchema: {
963
+ type: 'object',
964
+ properties: {
965
+ doc_id: { type: 'string', description: 'Document ID' },
966
+ section_index: { type: 'number', description: 'Section index' },
967
+ table_index: { type: 'number', description: 'Table index' },
968
+ row: { type: 'number', description: 'Row index' },
969
+ col: { type: 'number', description: 'Column index' },
970
+ width: { type: 'number', description: 'Cell width' },
971
+ height: { type: 'number', description: 'Cell height' },
972
+ background_color: { type: 'string', description: 'Background color (hex)' },
973
+ vertical_align: { type: 'string', enum: ['top', 'middle', 'bottom'], description: 'Vertical alignment' },
974
+ },
975
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col'],
976
+ },
977
+ },
978
+ {
979
+ name: 'merge_cells',
980
+ description: 'Merge multiple table cells into a single cell (HWPX only). The top-left cell becomes the master cell with increased colSpan/rowSpan.',
981
+ inputSchema: {
982
+ type: 'object',
983
+ properties: {
984
+ doc_id: { type: 'string', description: 'Document ID' },
985
+ section_index: { type: 'number', description: 'Section index' },
986
+ table_index: { type: 'number', description: 'Table index' },
987
+ start_row: { type: 'number', description: 'Starting row index (0-based)' },
988
+ start_col: { type: 'number', description: 'Starting column index (0-based)' },
989
+ end_row: { type: 'number', description: 'Ending row index (0-based, inclusive)' },
990
+ end_col: { type: 'number', description: 'Ending column index (0-based, inclusive)' },
991
+ },
992
+ required: ['doc_id', 'section_index', 'table_index', 'start_row', 'start_col', 'end_row', 'end_col'],
993
+ },
994
+ },
995
+ {
996
+ name: 'split_cell',
997
+ description: 'Split a merged table cell back into individual cells (HWPX only). Only works on cells with colSpan > 1 or rowSpan > 1.',
998
+ inputSchema: {
999
+ type: 'object',
1000
+ properties: {
1001
+ doc_id: { type: 'string', description: 'Document ID' },
1002
+ section_index: { type: 'number', description: 'Section index' },
1003
+ table_index: { type: 'number', description: 'Table index' },
1004
+ row: { type: 'number', description: 'Row index of the merged cell (0-based)' },
1005
+ col: { type: 'number', description: 'Column index of the merged cell (0-based)' },
1006
+ },
1007
+ required: ['doc_id', 'section_index', 'table_index', 'row', 'col'],
1008
+ },
1009
+ },
1010
+ {
1011
+ name: 'insert_table_row',
1012
+ description: 'Insert a new row in a table (HWPX only)',
1013
+ inputSchema: {
1014
+ type: 'object',
1015
+ properties: {
1016
+ doc_id: { type: 'string', description: 'Document ID' },
1017
+ section_index: { type: 'number', description: 'Section index' },
1018
+ table_index: { type: 'number', description: 'Table index' },
1019
+ after_row: { type: 'number', description: 'Insert after this row index (-1 for beginning)' },
1020
+ cell_texts: { type: 'array', items: { type: 'string' }, description: 'Text for each cell (optional)' },
1021
+ },
1022
+ required: ['doc_id', 'section_index', 'table_index', 'after_row'],
1023
+ },
1024
+ },
1025
+ {
1026
+ name: 'delete_table',
1027
+ description: 'Delete an entire table from the document (HWPX only)',
1028
+ inputSchema: {
1029
+ type: 'object',
1030
+ properties: {
1031
+ doc_id: { type: 'string', description: 'Document ID' },
1032
+ section_index: { type: 'number', description: 'Section index' },
1033
+ table_index: { type: 'number', description: 'Table index to delete' },
1034
+ },
1035
+ required: ['doc_id', 'section_index', 'table_index'],
1036
+ },
1037
+ },
1038
+ {
1039
+ name: 'delete_table_row',
1040
+ description: 'Delete a row from a table. If the table has only 1 row, deletes the entire table (HWPX only)',
1041
+ inputSchema: {
1042
+ type: 'object',
1043
+ properties: {
1044
+ doc_id: { type: 'string', description: 'Document ID' },
1045
+ section_index: { type: 'number', description: 'Section index' },
1046
+ table_index: { type: 'number', description: 'Table index' },
1047
+ row_index: { type: 'number', description: 'Row index to delete' },
1048
+ },
1049
+ required: ['doc_id', 'section_index', 'table_index', 'row_index'],
1050
+ },
1051
+ },
1052
+ {
1053
+ name: 'insert_table_column',
1054
+ description: 'Insert a new column in a table (HWPX only)',
1055
+ inputSchema: {
1056
+ type: 'object',
1057
+ properties: {
1058
+ doc_id: { type: 'string', description: 'Document ID' },
1059
+ section_index: { type: 'number', description: 'Section index' },
1060
+ table_index: { type: 'number', description: 'Table index' },
1061
+ after_col: { type: 'number', description: 'Insert after this column (-1 for beginning)' },
1062
+ },
1063
+ required: ['doc_id', 'section_index', 'table_index', 'after_col'],
1064
+ },
1065
+ },
1066
+ {
1067
+ name: 'delete_table_column',
1068
+ description: 'Delete a column from a table (HWPX only)',
1069
+ inputSchema: {
1070
+ type: 'object',
1071
+ properties: {
1072
+ doc_id: { type: 'string', description: 'Document ID' },
1073
+ section_index: { type: 'number', description: 'Section index' },
1074
+ table_index: { type: 'number', description: 'Table index' },
1075
+ col_index: { type: 'number', description: 'Column index to delete' },
1076
+ },
1077
+ required: ['doc_id', 'section_index', 'table_index', 'col_index'],
1078
+ },
1079
+ },
1080
+ {
1081
+ name: 'get_table_as_csv',
1082
+ description: 'Export table content as CSV format',
1083
+ inputSchema: {
1084
+ type: 'object',
1085
+ properties: {
1086
+ doc_id: { type: 'string', description: 'Document ID' },
1087
+ section_index: { type: 'number', description: 'Section index' },
1088
+ table_index: { type: 'number', description: 'Table index' },
1089
+ delimiter: { type: 'string', description: 'Delimiter character (default: comma)' },
1090
+ },
1091
+ required: ['doc_id', 'section_index', 'table_index'],
1092
+ },
1093
+ },
1094
+ // === Page Settings ===
1095
+ {
1096
+ name: 'get_page_settings',
1097
+ description: 'Get page settings (paper size, margins)',
1098
+ inputSchema: {
1099
+ type: 'object',
1100
+ properties: {
1101
+ doc_id: { type: 'string', description: 'Document ID' },
1102
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1103
+ },
1104
+ required: ['doc_id'],
1105
+ },
1106
+ },
1107
+ {
1108
+ name: 'set_page_settings',
1109
+ description: 'Set page settings (HWPX only)',
1110
+ inputSchema: {
1111
+ type: 'object',
1112
+ properties: {
1113
+ doc_id: { type: 'string', description: 'Document ID' },
1114
+ section_index: { type: 'number', description: 'Section index' },
1115
+ width: { type: 'number', description: 'Page width in pt' },
1116
+ height: { type: 'number', description: 'Page height in pt' },
1117
+ margin_top: { type: 'number', description: 'Top margin in pt' },
1118
+ margin_bottom: { type: 'number', description: 'Bottom margin in pt' },
1119
+ margin_left: { type: 'number', description: 'Left margin in pt' },
1120
+ margin_right: { type: 'number', description: 'Right margin in pt' },
1121
+ orientation: { type: 'string', enum: ['portrait', 'landscape'], description: 'Page orientation' },
1122
+ },
1123
+ required: ['doc_id'],
1124
+ },
1125
+ },
1126
+ // === Copy/Move ===
1127
+ {
1128
+ name: 'copy_paragraph',
1129
+ description: 'Copy a paragraph to another location (HWPX only)',
1130
+ inputSchema: {
1131
+ type: 'object',
1132
+ properties: {
1133
+ doc_id: { type: 'string', description: 'Document ID' },
1134
+ source_section: { type: 'number', description: 'Source section index' },
1135
+ source_paragraph: { type: 'number', description: 'Source paragraph index' },
1136
+ target_section: { type: 'number', description: 'Target section index' },
1137
+ target_after: { type: 'number', description: 'Insert after this paragraph in target' },
1138
+ },
1139
+ required: ['doc_id', 'source_section', 'source_paragraph', 'target_section', 'target_after'],
1140
+ },
1141
+ },
1142
+ {
1143
+ name: 'move_paragraph',
1144
+ description: 'Move a paragraph to another location (HWPX only)',
1145
+ inputSchema: {
1146
+ type: 'object',
1147
+ properties: {
1148
+ doc_id: { type: 'string', description: 'Document ID' },
1149
+ source_section: { type: 'number', description: 'Source section index' },
1150
+ source_paragraph: { type: 'number', description: 'Source paragraph index' },
1151
+ target_section: { type: 'number', description: 'Target section index' },
1152
+ target_after: { type: 'number', description: 'Insert after this paragraph in target' },
1153
+ },
1154
+ required: ['doc_id', 'source_section', 'source_paragraph', 'target_section', 'target_after'],
1155
+ },
1156
+ },
1157
+ {
1158
+ name: 'move_table',
1159
+ description: 'Move a table to another location (HWPX only). Uses XML-based approach for accurate structure preservation with strict validation.',
1160
+ inputSchema: {
1161
+ type: 'object',
1162
+ properties: {
1163
+ doc_id: { type: 'string', description: 'Document ID' },
1164
+ section_index: { type: 'number', description: 'Source section index' },
1165
+ table_index: { type: 'number', description: 'Table index within source section (0-based)' },
1166
+ target_section_index: { type: 'number', description: 'Target section index' },
1167
+ target_after_index: { type: 'number', description: 'Insert after this element index in target (-1 for beginning)' },
1168
+ },
1169
+ required: ['doc_id', 'section_index', 'table_index', 'target_section_index', 'target_after_index'],
1170
+ },
1171
+ },
1172
+ {
1173
+ name: 'copy_table',
1174
+ description: 'Copy a table to another location (HWPX only). Preserves original and generates new IDs for the copy. Uses strict validation.',
1175
+ inputSchema: {
1176
+ type: 'object',
1177
+ properties: {
1178
+ doc_id: { type: 'string', description: 'Document ID' },
1179
+ section_index: { type: 'number', description: 'Source section index' },
1180
+ table_index: { type: 'number', description: 'Table index within source section (0-based)' },
1181
+ target_section_index: { type: 'number', description: 'Target section index' },
1182
+ target_after_index: { type: 'number', description: 'Insert after this element index in target (-1 for beginning)' },
1183
+ },
1184
+ required: ['doc_id', 'section_index', 'table_index', 'target_section_index', 'target_after_index'],
1185
+ },
1186
+ },
1187
+ // === Statistics ===
1188
+ {
1189
+ name: 'get_word_count',
1190
+ description: 'Get word and character count statistics',
1191
+ inputSchema: {
1192
+ type: 'object',
1193
+ properties: {
1194
+ doc_id: { type: 'string', description: 'Document ID' },
1195
+ },
1196
+ required: ['doc_id'],
1197
+ },
1198
+ },
1199
+ // === Image Info (see "Image Operations" section for insert/render) ===
1200
+ {
1201
+ name: 'get_images',
1202
+ description: 'Get all images in the document. For inserting images, see insert_image or insert_image_in_cell.',
1203
+ inputSchema: {
1204
+ type: 'object',
1205
+ properties: {
1206
+ doc_id: { type: 'string', description: 'Document ID' },
1207
+ },
1208
+ required: ['doc_id'],
1209
+ },
1210
+ },
1211
+ // === Export ===
1212
+ {
1213
+ name: 'export_to_text',
1214
+ description: 'Export document to plain text file',
1215
+ inputSchema: {
1216
+ type: 'object',
1217
+ properties: {
1218
+ doc_id: { type: 'string', description: 'Document ID' },
1219
+ output_path: { type: 'string', description: 'Output file path' },
1220
+ },
1221
+ required: ['doc_id', 'output_path'],
1222
+ },
1223
+ },
1224
+ {
1225
+ name: 'export_to_html',
1226
+ description: 'Export document to HTML file',
1227
+ inputSchema: {
1228
+ type: 'object',
1229
+ properties: {
1230
+ doc_id: { type: 'string', description: 'Document ID' },
1231
+ output_path: { type: 'string', description: 'Output file path' },
1232
+ },
1233
+ required: ['doc_id', 'output_path'],
1234
+ },
1235
+ },
1236
+ // === Undo/Redo ===
1237
+ {
1238
+ name: 'undo',
1239
+ description: 'Undo the last change(s). Supports multiple undo with count parameter.',
1240
+ inputSchema: {
1241
+ type: 'object',
1242
+ properties: {
1243
+ doc_id: { type: 'string', description: 'Document ID' },
1244
+ count: { type: 'number', description: 'Number of times to undo (default: 1)' },
1245
+ },
1246
+ required: ['doc_id'],
1247
+ },
1248
+ },
1249
+ {
1250
+ name: 'redo',
1251
+ description: 'Redo the last undone change(s). Supports multiple redo with count parameter.',
1252
+ inputSchema: {
1253
+ type: 'object',
1254
+ properties: {
1255
+ doc_id: { type: 'string', description: 'Document ID' },
1256
+ count: { type: 'number', description: 'Number of times to redo (default: 1)' },
1257
+ },
1258
+ required: ['doc_id'],
1259
+ },
1260
+ },
1261
+ // === Table Creation ===
1262
+ {
1263
+ name: 'insert_table',
1264
+ description: 'Insert a new table (HWPX only)',
1265
+ inputSchema: {
1266
+ type: 'object',
1267
+ properties: {
1268
+ doc_id: { type: 'string', description: 'Document ID' },
1269
+ section_index: { type: 'number', description: 'Section index' },
1270
+ after_index: { type: 'number', description: 'Insert after this element index (-1 for beginning)' },
1271
+ rows: { type: 'number', description: 'Number of rows' },
1272
+ cols: { type: 'number', description: 'Number of columns' },
1273
+ width: { type: 'number', description: 'Table width (optional)' },
1274
+ },
1275
+ required: ['doc_id', 'section_index', 'after_index', 'rows', 'cols'],
1276
+ },
1277
+ },
1278
+ {
1279
+ name: 'insert_nested_table',
1280
+ description: 'Insert a table inside a table cell (nested table, HWPX only)',
1281
+ inputSchema: {
1282
+ type: 'object',
1283
+ properties: {
1284
+ doc_id: { type: 'string', description: 'Document ID' },
1285
+ section_index: { type: 'number', description: 'Section index' },
1286
+ parent_table_index: { type: 'number', description: 'Parent table index' },
1287
+ row: { type: 'number', description: 'Row index in parent table (0-based)' },
1288
+ col: { type: 'number', description: 'Column index in parent table (0-based)' },
1289
+ nested_rows: { type: 'number', description: 'Number of rows in nested table' },
1290
+ nested_cols: { type: 'number', description: 'Number of columns in nested table' },
1291
+ data: {
1292
+ type: 'array',
1293
+ description: 'Optional 2D array of cell data for nested table',
1294
+ items: {
1295
+ type: 'array',
1296
+ items: { type: 'string' }
1297
+ }
1298
+ },
1299
+ },
1300
+ required: ['doc_id', 'section_index', 'parent_table_index', 'row', 'col', 'nested_rows', 'nested_cols'],
1301
+ },
1302
+ },
1303
+ // === Header/Footer ===
1304
+ {
1305
+ name: 'get_header',
1306
+ description: 'Get header content for a section',
1307
+ inputSchema: {
1308
+ type: 'object',
1309
+ properties: {
1310
+ doc_id: { type: 'string', description: 'Document ID' },
1311
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1312
+ },
1313
+ required: ['doc_id'],
1314
+ },
1315
+ },
1316
+ {
1317
+ name: 'set_header',
1318
+ description: 'Set header content for a section (HWPX only)',
1319
+ inputSchema: {
1320
+ type: 'object',
1321
+ properties: {
1322
+ doc_id: { type: 'string', description: 'Document ID' },
1323
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1324
+ text: { type: 'string', description: 'Header text content' },
1325
+ apply_page_type: { type: 'string', enum: ['both', 'even', 'odd'], description: 'Apply to page type (default: both)' },
1326
+ },
1327
+ required: ['doc_id', 'text'],
1328
+ },
1329
+ },
1330
+ {
1331
+ name: 'get_footer',
1332
+ description: 'Get footer content for a section',
1333
+ inputSchema: {
1334
+ type: 'object',
1335
+ properties: {
1336
+ doc_id: { type: 'string', description: 'Document ID' },
1337
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1338
+ },
1339
+ required: ['doc_id'],
1340
+ },
1341
+ },
1342
+ {
1343
+ name: 'set_footer',
1344
+ description: 'Set footer content for a section (HWPX only)',
1345
+ inputSchema: {
1346
+ type: 'object',
1347
+ properties: {
1348
+ doc_id: { type: 'string', description: 'Document ID' },
1349
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1350
+ text: { type: 'string', description: 'Footer text content' },
1351
+ apply_page_type: { type: 'string', enum: ['both', 'even', 'odd'], description: 'Apply to page type (default: both)' },
1352
+ },
1353
+ required: ['doc_id', 'text'],
1354
+ },
1355
+ },
1356
+ // === Footnotes/Endnotes ===
1357
+ {
1358
+ name: 'get_footnotes',
1359
+ description: 'Get all footnotes in the document',
1360
+ inputSchema: {
1361
+ type: 'object',
1362
+ properties: {
1363
+ doc_id: { type: 'string', description: 'Document ID' },
1364
+ },
1365
+ required: ['doc_id'],
1366
+ },
1367
+ },
1368
+ {
1369
+ name: 'insert_footnote',
1370
+ description: 'Insert a footnote at a specific location (HWPX only)',
1371
+ inputSchema: {
1372
+ type: 'object',
1373
+ properties: {
1374
+ doc_id: { type: 'string', description: 'Document ID' },
1375
+ section_index: { type: 'number', description: 'Section index' },
1376
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1377
+ text: { type: 'string', description: 'Footnote text content' },
1378
+ },
1379
+ required: ['doc_id', 'section_index', 'paragraph_index', 'text'],
1380
+ },
1381
+ },
1382
+ {
1383
+ name: 'get_endnotes',
1384
+ description: 'Get all endnotes in the document',
1385
+ inputSchema: {
1386
+ type: 'object',
1387
+ properties: {
1388
+ doc_id: { type: 'string', description: 'Document ID' },
1389
+ },
1390
+ required: ['doc_id'],
1391
+ },
1392
+ },
1393
+ {
1394
+ name: 'insert_endnote',
1395
+ description: 'Insert an endnote at a specific location (HWPX only)',
1396
+ inputSchema: {
1397
+ type: 'object',
1398
+ properties: {
1399
+ doc_id: { type: 'string', description: 'Document ID' },
1400
+ section_index: { type: 'number', description: 'Section index' },
1401
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1402
+ text: { type: 'string', description: 'Endnote text content' },
1403
+ },
1404
+ required: ['doc_id', 'section_index', 'paragraph_index', 'text'],
1405
+ },
1406
+ },
1407
+ // === Bookmarks/Hyperlinks ===
1408
+ {
1409
+ name: 'get_bookmarks',
1410
+ description: 'Get all bookmarks in the document',
1411
+ inputSchema: {
1412
+ type: 'object',
1413
+ properties: {
1414
+ doc_id: { type: 'string', description: 'Document ID' },
1415
+ },
1416
+ required: ['doc_id'],
1417
+ },
1418
+ },
1419
+ {
1420
+ name: 'insert_bookmark',
1421
+ description: 'Insert a bookmark at a specific location (HWPX only)',
1422
+ inputSchema: {
1423
+ type: 'object',
1424
+ properties: {
1425
+ doc_id: { type: 'string', description: 'Document ID' },
1426
+ section_index: { type: 'number', description: 'Section index' },
1427
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1428
+ name: { type: 'string', description: 'Bookmark name' },
1429
+ },
1430
+ required: ['doc_id', 'section_index', 'paragraph_index', 'name'],
1431
+ },
1432
+ },
1433
+ {
1434
+ name: 'get_hyperlinks',
1435
+ description: 'Get all hyperlinks in the document',
1436
+ inputSchema: {
1437
+ type: 'object',
1438
+ properties: {
1439
+ doc_id: { type: 'string', description: 'Document ID' },
1440
+ },
1441
+ required: ['doc_id'],
1442
+ },
1443
+ },
1444
+ {
1445
+ name: 'insert_hyperlink',
1446
+ description: 'Insert a hyperlink in a paragraph (HWPX only)',
1447
+ inputSchema: {
1448
+ type: 'object',
1449
+ properties: {
1450
+ doc_id: { type: 'string', description: 'Document ID' },
1451
+ section_index: { type: 'number', description: 'Section index' },
1452
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1453
+ url: { type: 'string', description: 'URL for the hyperlink' },
1454
+ text: { type: 'string', description: 'Display text for the hyperlink' },
1455
+ },
1456
+ required: ['doc_id', 'section_index', 'paragraph_index', 'url', 'text'],
1457
+ },
1458
+ },
1459
+ // === Images ===
1460
+ {
1461
+ name: 'insert_image',
1462
+ description: `Insert an image as an independent element in the document (HWPX only). The image is placed OUTSIDE of tables, between paragraphs or after tables.
1463
+
1464
+ Use after_table or after_header for easier positioning.
1465
+
1466
+ ⚠️ WARNING: This tool ALWAYS inserts OUTSIDE tables. Even if after_header finds text inside a table cell, the image will be placed AFTER the table, not inside it.
1467
+
1468
+ 👉 To insert an image INSIDE a table cell:
1469
+ 1. First use find_insert_position_after_header to check found_in
1470
+ 2. If found_in='table_cell', use insert_image_in_cell with the returned table_info (table_index, row, col)
1471
+ 3. If found_in='paragraph', use this insert_image tool`,
1472
+ inputSchema: {
1473
+ type: 'object',
1474
+ properties: {
1475
+ doc_id: { type: 'string', description: 'Document ID' },
1476
+ section_index: { type: 'number', description: 'Section index (auto-detected if using after_table or after_header)' },
1477
+ after_index: { type: 'number', description: 'Insert after this element index. Use after_table or after_header instead for easier positioning.' },
1478
+ after_table: { type: 'number', description: 'RECOMMENDED: Insert after this table index (0-based global index from get_table_map). Automatically sets section_index and after_index.' },
1479
+ after_header: { type: 'string', description: 'RECOMMENDED: Insert after paragraph containing this text. Automatically sets section_index and after_index.' },
1480
+ image_path: { type: 'string', description: 'Path to the image file' },
1481
+ width: { type: 'number', description: 'Image width in points (optional). If only width is specified with preserve_aspect_ratio=true, height is auto-calculated.' },
1482
+ height: { type: 'number', description: 'Image height in points (optional). If only height is specified with preserve_aspect_ratio=true, width is auto-calculated.' },
1483
+ preserve_aspect_ratio: { type: 'boolean', description: 'If true, maintains original image aspect ratio. Default: false.' },
1484
+ position_type: { type: 'string', enum: ['inline', 'floating'], description: 'Position type: "inline" (flows with text like a character) or "floating" (positioned relative to anchor). Default: floating.' },
1485
+ vert_rel_to: { type: 'string', enum: ['para', 'paper'], description: 'Vertical reference point: "para" (paragraph) or "paper" (page). Default: para.' },
1486
+ horz_rel_to: { type: 'string', enum: ['column', 'para', 'paper'], description: 'Horizontal reference point: "column", "para" (paragraph), or "paper" (page). Default: column.' },
1487
+ vert_align: { type: 'string', enum: ['top', 'center', 'bottom'], description: 'Vertical alignment. Default: top.' },
1488
+ horz_align: { type: 'string', enum: ['left', 'center', 'right'], description: 'Horizontal alignment. Default: left.' },
1489
+ vert_offset: { type: 'number', description: 'Vertical offset from anchor in points. Default: 0.' },
1490
+ horz_offset: { type: 'number', description: 'Horizontal offset from anchor in points. Default: 0.' },
1491
+ text_wrap: { type: 'string', enum: ['top_and_bottom', 'square', 'tight', 'behind_text', 'in_front_of_text', 'none'], description: 'Text wrap mode. Default: top_and_bottom.' },
1492
+ },
1493
+ required: ['doc_id', 'image_path'],
1494
+ },
1495
+ },
1496
+ {
1497
+ name: 'update_image_size',
1498
+ description: 'Update the size of an existing image (HWPX only)',
1499
+ inputSchema: {
1500
+ type: 'object',
1501
+ properties: {
1502
+ doc_id: { type: 'string', description: 'Document ID' },
1503
+ section_index: { type: 'number', description: 'Section index' },
1504
+ image_index: { type: 'number', description: 'Image index within section' },
1505
+ width: { type: 'number', description: 'New width' },
1506
+ height: { type: 'number', description: 'New height' },
1507
+ },
1508
+ required: ['doc_id', 'section_index', 'image_index', 'width', 'height'],
1509
+ },
1510
+ },
1511
+ {
1512
+ name: 'delete_image',
1513
+ description: 'Delete an image from the document (HWPX only)',
1514
+ inputSchema: {
1515
+ type: 'object',
1516
+ properties: {
1517
+ doc_id: { type: 'string', description: 'Document ID' },
1518
+ section_index: { type: 'number', description: 'Section index' },
1519
+ image_index: { type: 'number', description: 'Image index within section' },
1520
+ },
1521
+ required: ['doc_id', 'section_index', 'image_index'],
1522
+ },
1523
+ },
1524
+ {
1525
+ name: 'render_mermaid',
1526
+ description: `Render a Mermaid diagram and insert it as an independent element OUTSIDE tables (HWPX only). Uses mermaid.ink API.
1527
+
1528
+ Use after_table or after_header for easier positioning.
1529
+
1530
+ ⚠️ WARNING: This tool ALWAYS inserts OUTSIDE tables. Even if after_header finds text inside a table cell, the diagram will be placed AFTER the table, not inside it.
1531
+
1532
+ 👉 To insert a Mermaid diagram INSIDE a table cell:
1533
+ 1. First use find_insert_position_after_header to check found_in
1534
+ 2. If found_in='table_cell', use render_mermaid_in_cell with the returned table_info (table_index, row, col)
1535
+ 3. If found_in='paragraph', use this render_mermaid tool`,
1536
+ inputSchema: {
1537
+ type: 'object',
1538
+ properties: {
1539
+ doc_id: { type: 'string', description: 'Document ID' },
1540
+ mermaid_code: { type: 'string', description: 'Mermaid diagram code (e.g., "graph TD; A-->B;")' },
1541
+ section_index: { type: 'number', description: 'Section index (auto-detected if using after_table or after_header)' },
1542
+ after_index: { type: 'number', description: 'Insert after this element index. Use after_table or after_header instead for easier positioning.' },
1543
+ after_table: { type: 'number', description: 'RECOMMENDED: Insert after this table index (0-based global index from get_table_map). Automatically sets section_index and after_index.' },
1544
+ after_header: { type: 'string', description: 'RECOMMENDED: Insert after paragraph containing this text. Automatically sets section_index and after_index.' },
1545
+ width: { type: 'number', description: 'Image width in points (optional). If specified with preserve_aspect_ratio=true, height is auto-calculated.' },
1546
+ height: { type: 'number', description: 'Image height in points (optional). If specified with preserve_aspect_ratio=true, width is auto-calculated.' },
1547
+ theme: { type: 'string', enum: ['default', 'dark', 'forest', 'neutral'], description: 'Diagram theme (default: default)' },
1548
+ background_color: { type: 'string', description: 'Background color (e.g., "#ffffff" or "transparent")' },
1549
+ preserve_aspect_ratio: { type: 'boolean', description: 'If true, maintains original image aspect ratio. Default: true for Mermaid diagrams.' },
1550
+ position_type: { type: 'string', enum: ['inline', 'floating'], description: 'Position type: "inline" (flows with text) or "floating" (positioned relative to anchor). Default: floating.' },
1551
+ vert_rel_to: { type: 'string', enum: ['para', 'paper'], description: 'Vertical reference point: "para" (paragraph) or "paper" (page). Default: para.' },
1552
+ horz_rel_to: { type: 'string', enum: ['column', 'para', 'paper'], description: 'Horizontal reference point: "column", "para" (paragraph), or "paper" (page). Default: column.' },
1553
+ vert_align: { type: 'string', enum: ['top', 'center', 'bottom'], description: 'Vertical alignment. Default: top.' },
1554
+ horz_align: { type: 'string', enum: ['left', 'center', 'right'], description: 'Horizontal alignment. Default: left.' },
1555
+ vert_offset: { type: 'number', description: 'Vertical offset from anchor in points. Default: 0.' },
1556
+ horz_offset: { type: 'number', description: 'Horizontal offset from anchor in points. Default: 0.' },
1557
+ text_wrap: { type: 'string', enum: ['top_and_bottom', 'square', 'tight', 'behind_text', 'in_front_of_text', 'none'], description: 'Text wrap mode. Default: top_and_bottom.' },
1558
+ },
1559
+ required: ['doc_id', 'mermaid_code'],
1560
+ },
1561
+ },
1562
+ {
1563
+ name: 'insert_image_in_cell',
1564
+ description: `📍 Insert an image INSIDE a specific table cell (HWPX only). The image appears inline within the cell content.
1565
+
1566
+ ⚠️ IMPORTANT: Use this tool (NOT insert_image) when inserting images into table cells!
1567
+
1568
+ When to use:
1569
+ 1. find_insert_position_after_header returned found_in='table_cell' → use table_info (table_index, row, col)
1570
+ 2. You want to add an image to a specific cell you already know
1571
+
1572
+ How to get table_index:
1573
+ - From find_insert_position_after_header result: table_info.table_index
1574
+ - Or use get_table_map to list all tables and find the index
1575
+
1576
+ Positioning within cell:
1577
+ - By default, image is inserted at the beginning of the cell
1578
+ - Use after_text to insert the image after a specific paragraph containing that text`,
1579
+ inputSchema: {
1580
+ type: 'object',
1581
+ properties: {
1582
+ doc_id: { type: 'string', description: 'Document ID' },
1583
+ table_index: { type: 'number', description: 'Global table index (0-based). Get from get_table_map.' },
1584
+ row: { type: 'number', description: 'Row index (0-based)' },
1585
+ col: { type: 'number', description: 'Column index (0-based)' },
1586
+ image_path: { type: 'string', description: 'Path to the image file' },
1587
+ width: { type: 'number', description: 'Image width in points (optional, default: 200)' },
1588
+ height: { type: 'number', description: 'Image height in points (optional, default: 150)' },
1589
+ preserve_aspect_ratio: { type: 'boolean', description: 'If true, maintains original image aspect ratio. Default: false.' },
1590
+ after_text: { type: 'string', description: 'Insert the image after the paragraph containing this text. If not found, falls back to beginning of cell.' },
1591
+ },
1592
+ required: ['doc_id', 'table_index', 'row', 'col', 'image_path'],
1593
+ },
1594
+ },
1595
+ {
1596
+ name: 'render_mermaid_in_cell',
1597
+ description: `📍 Render a Mermaid diagram and insert it INSIDE a specific table cell (HWPX only). Uses mermaid.ink API.
1598
+
1599
+ ⚠️ IMPORTANT: Use this tool (NOT render_mermaid) when inserting diagrams into table cells!
1600
+
1601
+ When to use:
1602
+ 1. find_insert_position_after_header returned found_in='table_cell' → use table_info (table_index, row, col)
1603
+ 2. You want to add a diagram to a specific cell you already know
1604
+
1605
+ How to get table_index:
1606
+ - From find_insert_position_after_header result: table_info.table_index
1607
+ - Or use get_table_map to list all tables and find the index
1608
+
1609
+ Positioning within cell:
1610
+ - By default, diagram is inserted at the beginning of the cell
1611
+ - Use after_text to insert the diagram after a specific paragraph containing that text`,
1612
+ inputSchema: {
1613
+ type: 'object',
1614
+ properties: {
1615
+ doc_id: { type: 'string', description: 'Document ID' },
1616
+ mermaid_code: { type: 'string', description: 'Mermaid diagram code (e.g., "graph TD; A-->B;")' },
1617
+ table_index: { type: 'number', description: 'Global table index (0-based). Get from get_table_map.' },
1618
+ row: { type: 'number', description: 'Row index (0-based)' },
1619
+ col: { type: 'number', description: 'Column index (0-based)' },
1620
+ width: { type: 'number', description: 'Image width in points (optional)' },
1621
+ height: { type: 'number', description: 'Image height in points (optional)' },
1622
+ theme: { type: 'string', enum: ['default', 'dark', 'forest', 'neutral'], description: 'Diagram theme (default: default)' },
1623
+ background_color: { type: 'string', description: 'Background color (e.g., "#ffffff" or "transparent")' },
1624
+ preserve_aspect_ratio: { type: 'boolean', description: 'If true, maintains original image aspect ratio. Default: true.' },
1625
+ after_text: { type: 'string', description: 'Insert the diagram after the paragraph containing this text. If not found, falls back to beginning of cell.' },
1626
+ },
1627
+ required: ['doc_id', 'mermaid_code', 'table_index', 'row', 'col'],
1628
+ },
1629
+ },
1630
+ // === Drawing Objects ===
1631
+ {
1632
+ name: 'insert_line',
1633
+ description: 'Insert a line drawing object (HWPX only)',
1634
+ inputSchema: {
1635
+ type: 'object',
1636
+ properties: {
1637
+ doc_id: { type: 'string', description: 'Document ID' },
1638
+ section_index: { type: 'number', description: 'Section index' },
1639
+ after_index: { type: 'number', description: 'Insert after this element index (-1 for beginning)' },
1640
+ x1: { type: 'number', description: 'Start X coordinate' },
1641
+ y1: { type: 'number', description: 'Start Y coordinate' },
1642
+ x2: { type: 'number', description: 'End X coordinate' },
1643
+ y2: { type: 'number', description: 'End Y coordinate' },
1644
+ stroke_color: { type: 'string', description: 'Stroke color (hex)' },
1645
+ stroke_width: { type: 'number', description: 'Stroke width' },
1646
+ },
1647
+ required: ['doc_id', 'section_index', 'after_index', 'x1', 'y1', 'x2', 'y2'],
1648
+ },
1649
+ },
1650
+ {
1651
+ name: 'insert_rect',
1652
+ description: 'Insert a rectangle drawing object (HWPX only)',
1653
+ inputSchema: {
1654
+ type: 'object',
1655
+ properties: {
1656
+ doc_id: { type: 'string', description: 'Document ID' },
1657
+ section_index: { type: 'number', description: 'Section index' },
1658
+ after_index: { type: 'number', description: 'Insert after this element index (-1 for beginning)' },
1659
+ x: { type: 'number', description: 'X coordinate' },
1660
+ y: { type: 'number', description: 'Y coordinate' },
1661
+ width: { type: 'number', description: 'Width' },
1662
+ height: { type: 'number', description: 'Height' },
1663
+ fill_color: { type: 'string', description: 'Fill color (hex)' },
1664
+ stroke_color: { type: 'string', description: 'Stroke color (hex)' },
1665
+ stroke_width: { type: 'number', description: 'Stroke width' },
1666
+ },
1667
+ required: ['doc_id', 'section_index', 'after_index', 'x', 'y', 'width', 'height'],
1668
+ },
1669
+ },
1670
+ {
1671
+ name: 'insert_ellipse',
1672
+ description: 'Insert an ellipse drawing object (HWPX only)',
1673
+ inputSchema: {
1674
+ type: 'object',
1675
+ properties: {
1676
+ doc_id: { type: 'string', description: 'Document ID' },
1677
+ section_index: { type: 'number', description: 'Section index' },
1678
+ after_index: { type: 'number', description: 'Insert after this element index (-1 for beginning)' },
1679
+ cx: { type: 'number', description: 'Center X coordinate' },
1680
+ cy: { type: 'number', description: 'Center Y coordinate' },
1681
+ rx: { type: 'number', description: 'Radius X' },
1682
+ ry: { type: 'number', description: 'Radius Y' },
1683
+ fill_color: { type: 'string', description: 'Fill color (hex)' },
1684
+ stroke_color: { type: 'string', description: 'Stroke color (hex)' },
1685
+ stroke_width: { type: 'number', description: 'Stroke width' },
1686
+ },
1687
+ required: ['doc_id', 'section_index', 'after_index', 'cx', 'cy', 'rx', 'ry'],
1688
+ },
1689
+ },
1690
+ // === Equations ===
1691
+ {
1692
+ name: 'get_equations',
1693
+ description: 'Get all equations in the document',
1694
+ inputSchema: {
1695
+ type: 'object',
1696
+ properties: {
1697
+ doc_id: { type: 'string', description: 'Document ID' },
1698
+ },
1699
+ required: ['doc_id'],
1700
+ },
1701
+ },
1702
+ {
1703
+ name: 'insert_equation',
1704
+ description: 'Insert an equation (HWPX only)',
1705
+ inputSchema: {
1706
+ type: 'object',
1707
+ properties: {
1708
+ doc_id: { type: 'string', description: 'Document ID' },
1709
+ section_index: { type: 'number', description: 'Section index' },
1710
+ after_index: { type: 'number', description: 'Insert after this element index (-1 for beginning)' },
1711
+ script: { type: 'string', description: 'Equation script (HWP equation format)' },
1712
+ },
1713
+ required: ['doc_id', 'section_index', 'after_index', 'script'],
1714
+ },
1715
+ },
1716
+ // === Memos ===
1717
+ {
1718
+ name: 'get_memos',
1719
+ description: 'Get all memos/comments in the document',
1720
+ inputSchema: {
1721
+ type: 'object',
1722
+ properties: {
1723
+ doc_id: { type: 'string', description: 'Document ID' },
1724
+ },
1725
+ required: ['doc_id'],
1726
+ },
1727
+ },
1728
+ {
1729
+ name: 'insert_memo',
1730
+ description: 'Insert a memo/comment (HWPX only)',
1731
+ inputSchema: {
1732
+ type: 'object',
1733
+ properties: {
1734
+ doc_id: { type: 'string', description: 'Document ID' },
1735
+ section_index: { type: 'number', description: 'Section index' },
1736
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1737
+ author: { type: 'string', description: 'Memo author' },
1738
+ content: { type: 'string', description: 'Memo content' },
1739
+ },
1740
+ required: ['doc_id', 'section_index', 'paragraph_index', 'content'],
1741
+ },
1742
+ },
1743
+ {
1744
+ name: 'delete_memo',
1745
+ description: 'Delete a memo/comment (HWPX only)',
1746
+ inputSchema: {
1747
+ type: 'object',
1748
+ properties: {
1749
+ doc_id: { type: 'string', description: 'Document ID' },
1750
+ memo_id: { type: 'string', description: 'Memo ID to delete' },
1751
+ },
1752
+ required: ['doc_id', 'memo_id'],
1753
+ },
1754
+ },
1755
+ // === Sections ===
1756
+ {
1757
+ name: 'get_sections',
1758
+ description: 'Get all sections in the document',
1759
+ inputSchema: {
1760
+ type: 'object',
1761
+ properties: {
1762
+ doc_id: { type: 'string', description: 'Document ID' },
1763
+ },
1764
+ required: ['doc_id'],
1765
+ },
1766
+ },
1767
+ {
1768
+ name: 'insert_section',
1769
+ description: 'Insert a new section (HWPX only)',
1770
+ inputSchema: {
1771
+ type: 'object',
1772
+ properties: {
1773
+ doc_id: { type: 'string', description: 'Document ID' },
1774
+ after_index: { type: 'number', description: 'Insert after this section index (-1 for beginning)' },
1775
+ },
1776
+ required: ['doc_id', 'after_index'],
1777
+ },
1778
+ },
1779
+ {
1780
+ name: 'delete_section',
1781
+ description: 'Delete a section (HWPX only)',
1782
+ inputSchema: {
1783
+ type: 'object',
1784
+ properties: {
1785
+ doc_id: { type: 'string', description: 'Document ID' },
1786
+ section_index: { type: 'number', description: 'Section index to delete' },
1787
+ },
1788
+ required: ['doc_id', 'section_index'],
1789
+ },
1790
+ },
1791
+ {
1792
+ name: 'get_section_xml',
1793
+ description: 'Get raw XML content of a section. Useful for AI-based document manipulation. Returns the complete section XML that can be modified and set back using set_section_xml.',
1794
+ inputSchema: {
1795
+ type: 'object',
1796
+ properties: {
1797
+ doc_id: { type: 'string', description: 'Document ID' },
1798
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1799
+ },
1800
+ required: ['doc_id'],
1801
+ },
1802
+ },
1803
+ {
1804
+ name: 'set_section_xml',
1805
+ description: 'Set (replace) raw XML content of a section (HWPX only). WARNING: This completely replaces the section XML. The XML must be valid HWPML format. Use get_section_xml first to get the current structure, modify it, then set it back.',
1806
+ inputSchema: {
1807
+ type: 'object',
1808
+ properties: {
1809
+ doc_id: { type: 'string', description: 'Document ID' },
1810
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1811
+ xml: { type: 'string', description: 'New XML content (must be valid HWPML section XML)' },
1812
+ validate: { type: 'boolean', description: 'Validate XML structure before replacing (default: true)' },
1813
+ },
1814
+ required: ['doc_id', 'xml'],
1815
+ },
1816
+ },
1817
+ // === Styles ===
1818
+ {
1819
+ name: 'get_styles',
1820
+ description: 'Get all defined styles in the document',
1821
+ inputSchema: {
1822
+ type: 'object',
1823
+ properties: {
1824
+ doc_id: { type: 'string', description: 'Document ID' },
1825
+ },
1826
+ required: ['doc_id'],
1827
+ },
1828
+ },
1829
+ {
1830
+ name: 'get_char_shapes',
1831
+ description: 'Get all character shape definitions',
1832
+ inputSchema: {
1833
+ type: 'object',
1834
+ properties: {
1835
+ doc_id: { type: 'string', description: 'Document ID' },
1836
+ },
1837
+ required: ['doc_id'],
1838
+ },
1839
+ },
1840
+ {
1841
+ name: 'get_para_shapes',
1842
+ description: 'Get all paragraph shape definitions',
1843
+ inputSchema: {
1844
+ type: 'object',
1845
+ properties: {
1846
+ doc_id: { type: 'string', description: 'Document ID' },
1847
+ },
1848
+ required: ['doc_id'],
1849
+ },
1850
+ },
1851
+ {
1852
+ name: 'apply_style',
1853
+ description: 'Apply a named style to a paragraph (HWPX only)',
1854
+ inputSchema: {
1855
+ type: 'object',
1856
+ properties: {
1857
+ doc_id: { type: 'string', description: 'Document ID' },
1858
+ section_index: { type: 'number', description: 'Section index' },
1859
+ paragraph_index: { type: 'number', description: 'Paragraph index' },
1860
+ style_id: { type: 'number', description: 'Style ID to apply' },
1861
+ },
1862
+ required: ['doc_id', 'section_index', 'paragraph_index', 'style_id'],
1863
+ },
1864
+ },
1865
+ // === Column Definition ===
1866
+ {
1867
+ name: 'get_column_def',
1868
+ description: 'Get column definition for a section',
1869
+ inputSchema: {
1870
+ type: 'object',
1871
+ properties: {
1872
+ doc_id: { type: 'string', description: 'Document ID' },
1873
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1874
+ },
1875
+ required: ['doc_id'],
1876
+ },
1877
+ },
1878
+ {
1879
+ name: 'set_column_def',
1880
+ description: 'Set column definition for a section (HWPX only)',
1881
+ inputSchema: {
1882
+ type: 'object',
1883
+ properties: {
1884
+ doc_id: { type: 'string', description: 'Document ID' },
1885
+ section_index: { type: 'number', description: 'Section index (default 0)' },
1886
+ count: { type: 'number', description: 'Number of columns' },
1887
+ type: { type: 'string', enum: ['newspaper', 'balanced', 'parallel'], description: 'Column type' },
1888
+ same_size: { type: 'boolean', description: 'Whether all columns have same width' },
1889
+ gap: { type: 'number', description: 'Gap between columns' },
1890
+ },
1891
+ required: ['doc_id', 'count'],
1892
+ },
1893
+ },
1894
+ // === New Document Creation ===
1895
+ {
1896
+ name: 'create_document',
1897
+ description: 'Create a new empty HWPX document',
1898
+ inputSchema: {
1899
+ type: 'object',
1900
+ properties: {
1901
+ title: { type: 'string', description: 'Document title (optional)' },
1902
+ creator: { type: 'string', description: 'Document author (optional)' },
1903
+ },
1904
+ },
1905
+ },
1906
+ // === XML Analysis and Repair ===
1907
+ {
1908
+ name: 'analyze_xml',
1909
+ description: 'Analyze document XML for issues like tag imbalance, malformed elements, etc. Useful for diagnosing save failures.',
1910
+ inputSchema: {
1911
+ type: 'object',
1912
+ properties: {
1913
+ doc_id: { type: 'string', description: 'Document ID' },
1914
+ section_index: { type: 'number', description: 'Section index to analyze (optional, analyzes all sections if not specified)' },
1915
+ },
1916
+ required: ['doc_id'],
1917
+ },
1918
+ },
1919
+ {
1920
+ name: 'repair_xml',
1921
+ description: 'Attempt to repair XML issues in a section. Removes orphan closing tags and fixes table structure.',
1922
+ inputSchema: {
1923
+ type: 'object',
1924
+ properties: {
1925
+ doc_id: { type: 'string', description: 'Document ID' },
1926
+ section_index: { type: 'number', description: 'Section index to repair' },
1927
+ remove_orphan_close_tags: { type: 'boolean', description: 'Remove orphan closing tags (default: true)' },
1928
+ fix_table_structure: { type: 'boolean', description: 'Fix table structure issues (default: true)' },
1929
+ backup: { type: 'boolean', description: 'Keep backup of original XML (default: true)' },
1930
+ },
1931
+ required: ['doc_id', 'section_index'],
1932
+ },
1933
+ },
1934
+ {
1935
+ name: 'get_raw_section_xml',
1936
+ description: `⚠️ DEPRECATED: Use get_section_xml instead. This tool is kept for backward compatibility only.`,
1937
+ inputSchema: {
1938
+ type: 'object',
1939
+ properties: {
1940
+ doc_id: { type: 'string', description: 'Document ID' },
1941
+ section_index: { type: 'number', description: 'Section index' },
1942
+ },
1943
+ required: ['doc_id', 'section_index'],
1944
+ },
1945
+ },
1946
+ {
1947
+ name: 'set_raw_section_xml',
1948
+ description: `⚠️ DEPRECATED: Use set_section_xml instead. This tool is kept for backward compatibility only.`,
1949
+ inputSchema: {
1950
+ type: 'object',
1951
+ properties: {
1952
+ doc_id: { type: 'string', description: 'Document ID' },
1953
+ section_index: { type: 'number', description: 'Section index' },
1954
+ xml: { type: 'string', description: 'New XML content (must be valid HWPML section XML)' },
1955
+ validate: { type: 'boolean', description: 'Validate XML structure before replacing (default: true)' },
1956
+ },
1957
+ required: ['doc_id', 'section_index', 'xml'],
1958
+ },
1959
+ },
1960
+ // ===== Agentic Document Reading Tools =====
1961
+ {
1962
+ name: 'chunk_document',
1963
+ description: `📖 Split document into overlapping chunks for agentic reading.
1964
+
1965
+ Use this for:
1966
+ - Large document analysis where full text would exceed context limits
1967
+ - Semantic search across document sections
1968
+ - Progressive document exploration
1969
+
1970
+ Returns array of chunks with:
1971
+ - Unique chunk ID for reference
1972
+ - Text content
1973
+ - Position offsets (global character positions)
1974
+ - Element type (paragraph/table/mixed)
1975
+ - Metadata (char count, word count, heading level)
1976
+
1977
+ Chunks are cached for performance. Call invalidate_reading_cache after document modifications.`,
1978
+ inputSchema: {
1979
+ type: 'object',
1980
+ properties: {
1981
+ doc_id: { type: 'string', description: 'Document ID' },
1982
+ chunk_size: { type: 'number', description: 'Target chunk size in characters (default: 500)' },
1983
+ overlap: { type: 'number', description: 'Overlap between chunks in characters (default: 100)' },
1984
+ },
1985
+ required: ['doc_id'],
1986
+ },
1987
+ },
1988
+ {
1989
+ name: 'search_chunks',
1990
+ description: `🔍 Search document chunks using BM25-based relevance scoring.
1991
+
1992
+ Returns chunks ranked by similarity to query with:
1993
+ - Relevance score (higher = more relevant)
1994
+ - Matched search terms
1995
+ - Text snippet around first match
1996
+ - Full chunk data with position info
1997
+
1998
+ Use for finding relevant sections in large documents without reading the entire content.`,
1999
+ inputSchema: {
2000
+ type: 'object',
2001
+ properties: {
2002
+ doc_id: { type: 'string', description: 'Document ID' },
2003
+ query: { type: 'string', description: 'Search query (keywords or phrase)' },
2004
+ top_k: { type: 'number', description: 'Number of top results to return (default: 5)' },
2005
+ min_score: { type: 'number', description: 'Minimum relevance score threshold (default: 0.1)' },
2006
+ },
2007
+ required: ['doc_id', 'query'],
2008
+ },
2009
+ },
2010
+ {
2011
+ name: 'get_chunk_context',
2012
+ description: `📄 Get surrounding chunks for expanded context around a specific chunk.
2013
+
2014
+ After finding a relevant chunk with search_chunks, use this to get additional context
2015
+ by retrieving chunks before and after the target chunk.`,
2016
+ inputSchema: {
2017
+ type: 'object',
2018
+ properties: {
2019
+ doc_id: { type: 'string', description: 'Document ID' },
2020
+ chunk_id: { type: 'string', description: 'ID of the center chunk (from search_chunks or chunk_document)' },
2021
+ before: { type: 'number', description: 'Number of chunks before to include (default: 1)' },
2022
+ after: { type: 'number', description: 'Number of chunks after to include (default: 1)' },
2023
+ },
2024
+ required: ['doc_id', 'chunk_id'],
2025
+ },
2026
+ },
2027
+ {
2028
+ name: 'extract_toc',
2029
+ description: `📋 Extract table of contents based on Korean document formatting conventions.
2030
+
2031
+ Detects headings by:
2032
+ - Roman numerals (I. II. III.)
2033
+ - Arabic numerals (1. 2. 3.)
2034
+ - Korean characters (가. 나. 다.)
2035
+ - Circled numbers (① ② ③)
2036
+ - Parenthesized numbers ((1) (2) (3))
2037
+ - Korean consonants (ㄱ. ㄴ. ㄷ.)
2038
+ - Bullet points (- • ◦)
2039
+
2040
+ Returns hierarchical TOC with level, title, section/element indices, and character offsets.`,
2041
+ inputSchema: {
2042
+ type: 'object',
2043
+ properties: {
2044
+ doc_id: { type: 'string', description: 'Document ID' },
2045
+ },
2046
+ required: ['doc_id'],
2047
+ },
2048
+ },
2049
+ {
2050
+ name: 'build_position_index',
2051
+ description: `🗂️ Build position index for document elements (headings, paragraphs, tables).
2052
+
2053
+ Creates a searchable index of all document elements with:
2054
+ - Unique ID
2055
+ - Element type (heading/paragraph/table/image)
2056
+ - Text preview (first 200 chars)
2057
+ - Section and element indices
2058
+ - Character offset
2059
+ - Heading level (if applicable)
2060
+ - Table info (rows, cols) for tables
2061
+
2062
+ Use get_position_index to retrieve cached index, or call this to rebuild after modifications.`,
2063
+ inputSchema: {
2064
+ type: 'object',
2065
+ properties: {
2066
+ doc_id: { type: 'string', description: 'Document ID' },
2067
+ },
2068
+ required: ['doc_id'],
2069
+ },
2070
+ },
2071
+ {
2072
+ name: 'get_position_index',
2073
+ description: `📍 Get cached position index (builds if not available).
2074
+
2075
+ Returns all indexed elements. Use search_position_index for filtered queries.`,
2076
+ inputSchema: {
2077
+ type: 'object',
2078
+ properties: {
2079
+ doc_id: { type: 'string', description: 'Document ID' },
2080
+ },
2081
+ required: ['doc_id'],
2082
+ },
2083
+ },
2084
+ {
2085
+ name: 'search_position_index',
2086
+ description: `🔎 Search position index by text and/or element type.
2087
+
2088
+ Filter the position index to find specific headings, paragraphs, or tables by their text content.`,
2089
+ inputSchema: {
2090
+ type: 'object',
2091
+ properties: {
2092
+ doc_id: { type: 'string', description: 'Document ID' },
2093
+ query: { type: 'string', description: 'Text to search for in element content' },
2094
+ type: { type: 'string', enum: ['heading', 'paragraph', 'table'], description: 'Filter by element type (optional)' },
2095
+ },
2096
+ required: ['doc_id', 'query'],
2097
+ },
2098
+ },
2099
+ {
2100
+ name: 'get_chunk_at_offset',
2101
+ description: `📌 Get the chunk containing a specific character offset.
2102
+
2103
+ Use after finding a position in the index to get the full chunk context.`,
2104
+ inputSchema: {
2105
+ type: 'object',
2106
+ properties: {
2107
+ doc_id: { type: 'string', description: 'Document ID' },
2108
+ offset: { type: 'number', description: 'Character offset in the document' },
2109
+ },
2110
+ required: ['doc_id', 'offset'],
2111
+ },
2112
+ },
2113
+ {
2114
+ name: 'invalidate_reading_cache',
2115
+ description: `🔄 Clear cached chunks and position index.
2116
+
2117
+ Call this after modifying the document to ensure fresh data on next read operation.`,
2118
+ inputSchema: {
2119
+ type: 'object',
2120
+ properties: {
2121
+ doc_id: { type: 'string', description: 'Document ID' },
2122
+ },
2123
+ required: ['doc_id'],
2124
+ },
2125
+ },
2126
+ ];
2127
+ // ============================================================
2128
+ // Server Setup
2129
+ // ============================================================
2130
+ const server = new index_js_1.Server({
2131
+ name: 'hwpx-mcp-server',
2132
+ version: '0.3.0',
2133
+ }, {
2134
+ capabilities: {
2135
+ tools: {},
2136
+ },
2137
+ });
2138
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools }));
2139
+ // ============================================================
2140
+ // Tool Handlers
2141
+ // ============================================================
2142
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2143
+ const { name, arguments: args } = request.params;
2144
+ try {
2145
+ switch (name) {
2146
+ // === 🎯 Tool Guide ===
2147
+ case 'get_tool_guide': {
2148
+ const workflow = args?.workflow;
2149
+ const guides = {
2150
+ template: `📋 TEMPLATE/FORM WORKFLOW (양식 작업)
2151
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2152
+
2153
+ ⭐ CORE TOOLS (these preserve existing styles):
2154
+ 1. open_document - Open the template file
2155
+ 2. get_table_map - Find all tables with their headers
2156
+ 3. update_table_cell - Fill table cells (keeps formatting!)
2157
+ 4. update_paragraph_text - Fill paragraphs (keeps formatting!)
2158
+ 5. save_document - Save changes
2159
+
2160
+ 💡 KEY INSIGHT:
2161
+ When working with templates, use update_* tools instead of replace_*.
2162
+ They preserve the original formatting (font, alignment, size).
2163
+
2164
+ 📝 EXAMPLE WORKFLOW:
2165
+ 1. open_document({ file_path: "template.hwpx" })
2166
+ 2. get_table_map({ doc_id: "..." }) → find target table
2167
+ 3. update_table_cell({ ..., text: "새 내용" }) → fill cells
2168
+ 4. save_document({ doc_id: "...", output_path: "filled.hwpx" })`,
2169
+ table: `📊 TABLE WORKFLOW (테이블 작업)
2170
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2171
+
2172
+ ⭐ FINDING TABLES:
2173
+ - get_table_map ⭐ - Best! Returns all tables with headers
2174
+ - find_table_by_header - Search by header text
2175
+ - get_tables - Raw table list
2176
+
2177
+ ⭐ READING TABLE DATA:
2178
+ - get_table - Get full table data
2179
+ - get_table_cell - Get specific cell content
2180
+ - get_table_as_csv - Export as CSV
2181
+
2182
+ ⭐ MODIFYING TABLES:
2183
+ - update_table_cell ⭐ - Update cell content (preserves style)
2184
+ - replace_text_in_cell - Find/replace within cell
2185
+ - set_cell_properties - Change cell formatting
2186
+ - merge_cells / split_cell - Merge or split cells
2187
+
2188
+ ⭐ CREATING TABLES:
2189
+ - insert_table - Create new table
2190
+ - insert_table_row / insert_table_column - Add rows/columns
2191
+ - delete_table_row / delete_table_column - Remove rows/columns`,
2192
+ image: `🖼️ IMAGE WORKFLOW (이미지 삽입)
2193
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2194
+
2195
+ ⚠️ CRITICAL: Choose the RIGHT tool based on location!
2196
+
2197
+ 📍 OUTSIDE tables (between paragraphs):
2198
+ - insert_image - Insert image file
2199
+ - render_mermaid - Insert Mermaid diagram
2200
+
2201
+ 📍 INSIDE table cells:
2202
+ - insert_image_in_cell ⭐ - Insert image INTO a cell
2203
+ - render_mermaid_in_cell - Insert diagram INTO a cell
2204
+
2205
+ 🔍 FINDING THE RIGHT POSITION:
2206
+ 1. find_insert_position_after_header({ header_text: "..." })
2207
+ 2. Check the 'found_in' field in result:
2208
+ - found_in='paragraph' → use insert_image
2209
+ - found_in='table_cell' → use insert_image_in_cell with table_info
2210
+
2211
+ 💡 COMMON MISTAKE:
2212
+ Using insert_image when text is inside a table cell.
2213
+ The image will appear AFTER the table, not inside the cell!`,
2214
+ search: `🔍 SEARCH & REPLACE WORKFLOW
2215
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2216
+
2217
+ ⭐ SEARCHING:
2218
+ - search_text - Find text in document (paragraphs + tables)
2219
+ - find_paragraph_by_text - Find paragraph by content
2220
+ - find_table_by_header - Find table by header
2221
+
2222
+ ⭐ REPLACING:
2223
+ - replace_text - Replace throughout ENTIRE document
2224
+ - replace_text_in_cell - Replace within specific cell
2225
+ - batch_replace - Multiple replacements at once
2226
+
2227
+ 💡 WHEN TO USE WHICH:
2228
+ - Bulk replacement (2024→2025 everywhere) → replace_text
2229
+ - Specific cell only → replace_text_in_cell
2230
+ - Specific paragraph → update_paragraph_text`,
2231
+ read: `📖 READING/ANALYZING WORKFLOW
2232
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2233
+
2234
+ ⭐ QUICK OVERVIEW:
2235
+ - get_document_text - Get all text content
2236
+ - get_document_structure - See sections, paragraphs, tables count
2237
+ - get_document_outline - Hierarchical TOC-like view
2238
+ - extract_toc - Extract table of contents
2239
+
2240
+ ⭐ DETAILED READING:
2241
+ - get_paragraphs - Get all paragraphs with details
2242
+ - get_table_map - Get all tables with headers
2243
+ - get_table - Get specific table data
2244
+
2245
+ ⭐ LARGE DOCUMENT ANALYSIS (Agentic):
2246
+ - chunk_document - Split into chunks for analysis
2247
+ - search_chunks - Search within chunks
2248
+ - build_position_index - Create searchable index`,
2249
+ create: `📝 CREATE NEW DOCUMENT WORKFLOW
2250
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2251
+
2252
+ ⭐ BASIC CREATION:
2253
+ 1. create_document({ title: "..." }) - Create empty document
2254
+ 2. insert_paragraph - Add paragraphs
2255
+ 3. insert_table - Add tables
2256
+ 4. save_document - Save to file
2257
+
2258
+ ⭐ STYLING:
2259
+ - set_paragraph_style - Set alignment, line spacing
2260
+ - set_text_style - Set font, size, color
2261
+ - set_auto_hanging_indent - Auto hanging indent for markers
2262
+
2263
+ ⚠️ NOTE:
2264
+ Creating styled documents from scratch is complex.
2265
+ For best results, start with a template file instead.`,
2266
+ all: `📚 COMPLETE TOOL REFERENCE
2267
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2268
+
2269
+ 🎯 TOP 10 MOST USED TOOLS:
2270
+ 1. open_document / save_document / close_document
2271
+ 2. get_table_map ⭐ - Find tables
2272
+ 3. update_table_cell ⭐ - Fill table cells
2273
+ 4. update_paragraph_text ⭐ - Fill paragraphs
2274
+ 5. search_text / replace_text - Find/replace
2275
+ 6. insert_image / insert_image_in_cell - Add images
2276
+ 7. get_document_text - Read content
2277
+ 8. set_auto_hanging_indent - Format lists
2278
+ 9. render_mermaid / render_mermaid_in_cell - Add diagrams
2279
+ 10. get_document_outline - Document structure
2280
+
2281
+ 📁 CATEGORIES:
2282
+ - Document: open, save, close, create
2283
+ - Paragraphs: get, insert, update, delete, style
2284
+ - Tables: get_table_map, get_table, update_cell, insert
2285
+ - Images: insert_image, insert_image_in_cell, render_mermaid
2286
+ - Search: search_text, replace_text, batch_replace
2287
+ - Formatting: set_paragraph_style, set_text_style, hanging_indent
2288
+ - Advanced: XML operations, chunking, position index
2289
+
2290
+ 💡 WORKFLOW GUIDES:
2291
+ Call get_tool_guide with: template, table, image, search, read, create`
2292
+ };
2293
+ const guide = guides[workflow] || guides['all'];
2294
+ return success({ workflow, guide });
2295
+ }
2296
+ // === Document Management ===
2297
+ case 'open_document': {
2298
+ const filePath = args?.file_path;
2299
+ if (!filePath)
2300
+ return error('file_path is required');
2301
+ const absolutePath = path.resolve(filePath);
2302
+ const data = fs.readFileSync(absolutePath);
2303
+ const docId = generateId();
2304
+ const doc = await HwpxDocument_1.HwpxDocument.createFromBuffer(docId, absolutePath, data);
2305
+ openDocuments.set(docId, doc);
2306
+ return success({
2307
+ doc_id: docId,
2308
+ format: doc.format,
2309
+ path: absolutePath,
2310
+ structure: doc.getStructure(),
2311
+ metadata: doc.getMetadata(),
2312
+ });
2313
+ }
2314
+ case 'close_document': {
2315
+ const docId = args?.doc_id;
2316
+ if (openDocuments.delete(docId)) {
2317
+ return success({ message: 'Document closed' });
2318
+ }
2319
+ return error('Document not found');
2320
+ }
2321
+ case 'save_document': {
2322
+ const docId = args?.doc_id;
2323
+ const doc = getDoc(docId);
2324
+ if (!doc)
2325
+ return error('Document not found');
2326
+ if (doc.format === 'hwp')
2327
+ return error('HWP files are read-only');
2328
+ // Use document lock to ensure all pending updates complete before save
2329
+ return await withDocumentLock(docId, async () => {
2330
+ const savePath = args?.output_path || doc.path;
2331
+ const createBackup = args?.create_backup !== false; // default: true
2332
+ const verifyIntegrity = args?.verify_integrity !== false; // default: true
2333
+ let backupPath = null;
2334
+ const tempPath = savePath + '.tmp';
2335
+ // Create backup if file exists and backup is enabled
2336
+ if (createBackup && fs.existsSync(savePath)) {
2337
+ backupPath = savePath + '.bak';
2338
+ try {
2339
+ fs.copyFileSync(savePath, backupPath);
2340
+ }
2341
+ catch (backupErr) {
2342
+ return error(`Failed to create backup: ${backupErr}`);
2343
+ }
2344
+ }
2345
+ try {
2346
+ const data = await doc.save();
2347
+ // Phase 1: Write to temp file first (atomic write pattern)
2348
+ fs.writeFileSync(tempPath, data);
2349
+ // Verify integrity on temp file before moving
2350
+ if (verifyIntegrity) {
2351
+ try {
2352
+ const JSZip = require('jszip');
2353
+ const savedData = fs.readFileSync(tempPath);
2354
+ const zip = await JSZip.loadAsync(savedData);
2355
+ // Check essential HWPX structure files
2356
+ const requiredFiles = [
2357
+ 'mimetype',
2358
+ 'Contents/content.hpf',
2359
+ 'Contents/header.xml',
2360
+ 'Contents/section0.xml'
2361
+ ];
2362
+ const missingFiles = [];
2363
+ for (const requiredFile of requiredFiles) {
2364
+ if (!zip.file(requiredFile)) {
2365
+ missingFiles.push(requiredFile);
2366
+ }
2367
+ }
2368
+ if (missingFiles.length > 0) {
2369
+ throw new Error(`Missing required files: ${missingFiles.join(', ')}`);
2370
+ }
2371
+ // Verify all section XML files are valid
2372
+ const sectionFiles = Object.keys(zip.files).filter(f => f.match(/^Contents\/section\d+\.xml$/));
2373
+ for (const sectionFile of sectionFiles) {
2374
+ const file = zip.file(sectionFile);
2375
+ if (file) {
2376
+ const xmlContent = await file.async('string');
2377
+ if (!xmlContent || !xmlContent.includes('<?xml')) {
2378
+ throw new Error(`Invalid XML in ${sectionFile}`);
2379
+ }
2380
+ // Check for truncated XML (incomplete tag at end)
2381
+ if (xmlContent.match(/<[^>]*$/)) {
2382
+ throw new Error(`Truncated XML in ${sectionFile}`);
2383
+ }
2384
+ // Check for broken opening tags (< followed by < without >)
2385
+ if (xmlContent.match(/<[^>]*</)) {
2386
+ throw new Error(`Broken tag structure in ${sectionFile}`);
2387
+ }
2388
+ }
2389
+ }
2390
+ }
2391
+ catch (verifyErr) {
2392
+ // Clean up temp file
2393
+ if (fs.existsSync(tempPath)) {
2394
+ fs.unlinkSync(tempPath);
2395
+ }
2396
+ // Restore from backup if exists
2397
+ if (backupPath && fs.existsSync(backupPath)) {
2398
+ return error(`Save verification failed, backup preserved: ${verifyErr}`);
2399
+ }
2400
+ return error(`Save verification failed: ${verifyErr}`);
2401
+ }
2402
+ }
2403
+ // Phase 2: Atomic move - rename temp to final (atomic on same filesystem)
2404
+ if (fs.existsSync(savePath)) {
2405
+ fs.unlinkSync(savePath);
2406
+ }
2407
+ fs.renameSync(tempPath, savePath);
2408
+ return success({
2409
+ message: `Saved to ${savePath}`,
2410
+ backup_created: backupPath ? true : false,
2411
+ integrity_verified: verifyIntegrity
2412
+ });
2413
+ }
2414
+ catch (saveErr) {
2415
+ // Clean up temp file if exists
2416
+ if (fs.existsSync(tempPath)) {
2417
+ try {
2418
+ fs.unlinkSync(tempPath);
2419
+ }
2420
+ catch { }
2421
+ }
2422
+ // Restore from backup if save fails
2423
+ if (backupPath && fs.existsSync(backupPath)) {
2424
+ fs.copyFileSync(backupPath, savePath);
2425
+ return error(`Save failed, restored from backup: ${saveErr}`);
2426
+ }
2427
+ return error(`Save failed: ${saveErr}`);
2428
+ }
2429
+ });
2430
+ }
2431
+ case 'list_open_documents': {
2432
+ const docs = Array.from(openDocuments.values()).map(d => ({
2433
+ id: d.id,
2434
+ path: d.path,
2435
+ format: d.format,
2436
+ isDirty: d.isDirty,
2437
+ }));
2438
+ return success({ documents: docs });
2439
+ }
2440
+ // === Document Info ===
2441
+ case 'get_document_text': {
2442
+ const doc = getDoc(args?.doc_id);
2443
+ if (!doc)
2444
+ return error('Document not found');
2445
+ return success({ text: doc.getAllText() });
2446
+ }
2447
+ case 'get_document_structure': {
2448
+ const doc = getDoc(args?.doc_id);
2449
+ if (!doc)
2450
+ return error('Document not found');
2451
+ return success(doc.getStructure());
2452
+ }
2453
+ case 'get_document_metadata': {
2454
+ const doc = getDoc(args?.doc_id);
2455
+ if (!doc)
2456
+ return error('Document not found');
2457
+ return success({ metadata: doc.getMetadata() });
2458
+ }
2459
+ case 'set_document_metadata': {
2460
+ const doc = getDoc(args?.doc_id);
2461
+ if (!doc)
2462
+ return error('Document not found');
2463
+ if (doc.format === 'hwp')
2464
+ return error('HWP files are read-only');
2465
+ const metadata = {};
2466
+ if (args?.title)
2467
+ metadata.title = args.title;
2468
+ if (args?.creator)
2469
+ metadata.creator = args.creator;
2470
+ if (args?.subject)
2471
+ metadata.subject = args.subject;
2472
+ if (args?.description)
2473
+ metadata.description = args.description;
2474
+ doc.setMetadata(metadata);
2475
+ return success({ metadata: doc.getMetadata() });
2476
+ }
2477
+ // === Paragraph Operations ===
2478
+ case 'get_paragraphs': {
2479
+ const doc = getDoc(args?.doc_id);
2480
+ if (!doc)
2481
+ return error('Document not found');
2482
+ const sectionIndex = args?.section_index;
2483
+ const paragraphs = doc.getParagraphs(sectionIndex);
2484
+ return success({ paragraphs });
2485
+ }
2486
+ case 'get_paragraph': {
2487
+ const doc = getDoc(args?.doc_id);
2488
+ if (!doc)
2489
+ return error('Document not found');
2490
+ const result = doc.getParagraph(args?.section_index, args?.paragraph_index);
2491
+ if (!result)
2492
+ return error('Paragraph not found');
2493
+ return success(result);
2494
+ }
2495
+ case 'insert_paragraph': {
2496
+ const doc = getDoc(args?.doc_id);
2497
+ if (!doc)
2498
+ return error('Document not found');
2499
+ if (doc.format === 'hwp')
2500
+ return error('HWP files are read-only');
2501
+ const sectionIndex = args?.section_index;
2502
+ const index = doc.insertParagraph(sectionIndex, args?.after_index, args?.text);
2503
+ if (index === -1)
2504
+ return error('Failed to insert paragraph');
2505
+ // Auto hanging indent (default: true)
2506
+ const autoHangingIndent = args?.auto_hanging_indent !== false;
2507
+ let indentPt = 0;
2508
+ if (autoHangingIndent) {
2509
+ // Use async version to read font size from document
2510
+ indentPt = await doc.setAutoHangingIndentAsync(sectionIndex, index, 10);
2511
+ }
2512
+ if (indentPt > 0) {
2513
+ return success({ message: `Paragraph inserted with hanging indent: ${indentPt.toFixed(2)}pt`, index, indent_pt: indentPt });
2514
+ }
2515
+ return success({ message: 'Paragraph inserted', index });
2516
+ }
2517
+ case 'delete_paragraph': {
2518
+ const doc = getDoc(args?.doc_id);
2519
+ if (!doc)
2520
+ return error('Document not found');
2521
+ if (doc.format === 'hwp')
2522
+ return error('HWP files are read-only');
2523
+ if (doc.deleteParagraph(args?.section_index, args?.paragraph_index)) {
2524
+ return success({ message: 'Paragraph deleted' });
2525
+ }
2526
+ return error('Failed to delete paragraph');
2527
+ }
2528
+ case 'update_paragraph_text': {
2529
+ const doc = getDoc(args?.doc_id);
2530
+ if (!doc)
2531
+ return error('Document not found');
2532
+ if (doc.format === 'hwp')
2533
+ return error('HWP files are read-only');
2534
+ const sectionIndex = args?.section_index;
2535
+ const paragraphIndex = args?.paragraph_index;
2536
+ const text = args?.text;
2537
+ // Auto-use preserve styles method for multi-run paragraphs
2538
+ const para = doc.getParagraph(sectionIndex, paragraphIndex);
2539
+ if (para && para.runs && para.runs.length > 1) {
2540
+ doc.updateParagraphTextPreserveStyles(sectionIndex, paragraphIndex, text);
2541
+ }
2542
+ else {
2543
+ doc.updateParagraphText(sectionIndex, paragraphIndex, args?.run_index ?? 0, text);
2544
+ }
2545
+ return success({ message: 'Paragraph updated' });
2546
+ }
2547
+ case 'update_paragraph_text_preserve_styles': {
2548
+ const doc = getDoc(args?.doc_id);
2549
+ if (!doc)
2550
+ return error('Document not found');
2551
+ if (doc.format === 'hwp')
2552
+ return error('HWP files are read-only');
2553
+ const result = doc.updateParagraphTextPreserveStyles(args?.section_index, args?.paragraph_index, args?.text);
2554
+ if (result) {
2555
+ return success({ message: 'Paragraph text updated with preserved styles' });
2556
+ }
2557
+ return error('Failed to update paragraph (not found or no runs)');
2558
+ }
2559
+ case 'append_text_to_paragraph': {
2560
+ const doc = getDoc(args?.doc_id);
2561
+ if (!doc)
2562
+ return error('Document not found');
2563
+ if (doc.format === 'hwp')
2564
+ return error('HWP files are read-only');
2565
+ doc.appendTextToParagraph(args?.section_index, args?.paragraph_index, args?.text);
2566
+ return success({ message: 'Text appended' });
2567
+ }
2568
+ // === Character Styling ===
2569
+ case 'set_text_style': {
2570
+ const doc = getDoc(args?.doc_id);
2571
+ if (!doc)
2572
+ return error('Document not found');
2573
+ if (doc.format === 'hwp')
2574
+ return error('HWP files are read-only');
2575
+ const style = {};
2576
+ if (args?.bold !== undefined)
2577
+ style.bold = args.bold;
2578
+ if (args?.italic !== undefined)
2579
+ style.italic = args.italic;
2580
+ if (args?.underline !== undefined)
2581
+ style.underline = args.underline;
2582
+ if (args?.strikethrough !== undefined)
2583
+ style.strikethrough = args.strikethrough;
2584
+ if (args?.font_name)
2585
+ style.fontName = args.font_name;
2586
+ if (args?.font_size)
2587
+ style.fontSize = args.font_size;
2588
+ if (args?.font_color)
2589
+ style.fontColor = args.font_color;
2590
+ if (args?.background_color)
2591
+ style.backgroundColor = args.background_color;
2592
+ doc.applyCharacterStyle(args?.section_index, args?.paragraph_index, args?.run_index ?? 0, style);
2593
+ return success({ message: 'Text style applied' });
2594
+ }
2595
+ case 'get_text_style': {
2596
+ const doc = getDoc(args?.doc_id);
2597
+ if (!doc)
2598
+ return error('Document not found');
2599
+ const style = doc.getCharacterStyle(args?.section_index, args?.paragraph_index, args?.run_index);
2600
+ return success({ style });
2601
+ }
2602
+ // === Paragraph Styling ===
2603
+ case 'set_paragraph_style': {
2604
+ const doc = getDoc(args?.doc_id);
2605
+ if (!doc)
2606
+ return error('Document not found');
2607
+ if (doc.format === 'hwp')
2608
+ return error('HWP files are read-only');
2609
+ const style = {};
2610
+ if (args?.align)
2611
+ style.align = args.align;
2612
+ if (args?.line_spacing)
2613
+ style.lineSpacing = args.line_spacing;
2614
+ if (args?.margin_left)
2615
+ style.marginLeft = args.margin_left;
2616
+ if (args?.margin_right)
2617
+ style.marginRight = args.margin_right;
2618
+ if (args?.margin_top)
2619
+ style.marginTop = args.margin_top;
2620
+ if (args?.margin_bottom)
2621
+ style.marginBottom = args.margin_bottom;
2622
+ if (args?.first_line_indent)
2623
+ style.firstLineIndent = args.first_line_indent;
2624
+ doc.applyParagraphStyle(args?.section_index, args?.paragraph_index, style);
2625
+ return success({ message: 'Paragraph style applied' });
2626
+ }
2627
+ case 'get_paragraph_style': {
2628
+ const doc = getDoc(args?.doc_id);
2629
+ if (!doc)
2630
+ return error('Document not found');
2631
+ const style = doc.getParagraphStyle(args?.section_index, args?.paragraph_index);
2632
+ return success({ style });
2633
+ }
2634
+ // === Hanging Indent (내어쓰기) ===
2635
+ case 'set_hanging_indent': {
2636
+ const doc = getDoc(args?.doc_id);
2637
+ if (!doc)
2638
+ return error('Document not found');
2639
+ if (doc.format === 'hwp')
2640
+ return error('HWP files are read-only');
2641
+ const result = doc.setHangingIndent(args?.section_index, args?.paragraph_index, args?.indent_pt);
2642
+ if (!result)
2643
+ return error('Failed to set hanging indent. Check section/paragraph indices and indent value (must be positive).');
2644
+ return success({ message: `Hanging indent set to ${args?.indent_pt}pt` });
2645
+ }
2646
+ case 'get_hanging_indent': {
2647
+ const doc = getDoc(args?.doc_id);
2648
+ if (!doc)
2649
+ return error('Document not found');
2650
+ const indent = doc.getHangingIndent(args?.section_index, args?.paragraph_index);
2651
+ if (indent === null)
2652
+ return error('Invalid section or paragraph index');
2653
+ return success({ hanging_indent_pt: indent });
2654
+ }
2655
+ case 'remove_hanging_indent': {
2656
+ const doc = getDoc(args?.doc_id);
2657
+ if (!doc)
2658
+ return error('Document not found');
2659
+ if (doc.format === 'hwp')
2660
+ return error('HWP files are read-only');
2661
+ const result = doc.removeHangingIndent(args?.section_index, args?.paragraph_index);
2662
+ if (!result)
2663
+ return error('Failed to remove hanging indent. Check section/paragraph indices.');
2664
+ return success({ message: 'Hanging indent removed' });
2665
+ }
2666
+ // === Table Cell Hanging Indent (테이블 셀 내어쓰기) ===
2667
+ case 'set_table_cell_hanging_indent': {
2668
+ const doc = getDoc(args?.doc_id);
2669
+ if (!doc)
2670
+ return error('Document not found');
2671
+ if (doc.format === 'hwp')
2672
+ return error('HWP files are read-only');
2673
+ const result = doc.setTableCellHangingIndent(args?.section_index, args?.table_index, args?.row, args?.col, args?.paragraph_index, args?.indent_pt);
2674
+ if (!result)
2675
+ return error('Failed to set hanging indent. Check indices and indent value (must be positive).');
2676
+ return success({ message: `Table cell hanging indent set to ${args?.indent_pt}pt` });
2677
+ }
2678
+ case 'get_table_cell_hanging_indent': {
2679
+ const doc = getDoc(args?.doc_id);
2680
+ if (!doc)
2681
+ return error('Document not found');
2682
+ const indent = doc.getTableCellHangingIndent(args?.section_index, args?.table_index, args?.row, args?.col, args?.paragraph_index);
2683
+ if (indent === null)
2684
+ return error('Invalid indices (section, table, row, col, or paragraph)');
2685
+ return success({ hanging_indent_pt: indent });
2686
+ }
2687
+ case 'remove_table_cell_hanging_indent': {
2688
+ const doc = getDoc(args?.doc_id);
2689
+ if (!doc)
2690
+ return error('Document not found');
2691
+ if (doc.format === 'hwp')
2692
+ return error('HWP files are read-only');
2693
+ const result = doc.removeTableCellHangingIndent(args?.section_index, args?.table_index, args?.row, args?.col, args?.paragraph_index);
2694
+ if (!result)
2695
+ return error('Failed to remove hanging indent. Check indices.');
2696
+ return success({ message: 'Table cell hanging indent removed' });
2697
+ }
2698
+ case 'set_auto_hanging_indent': {
2699
+ const doc = getDoc(args?.doc_id);
2700
+ if (!doc)
2701
+ return error('Document not found');
2702
+ if (doc.format === 'hwp')
2703
+ return error('HWP files are read-only');
2704
+ // Use async version to read actual font size from document
2705
+ const fontSizeArg = args?.font_size;
2706
+ const indentPt = fontSizeArg !== undefined
2707
+ ? doc.setAutoHangingIndent(args?.section_index, args?.paragraph_index, fontSizeArg)
2708
+ : await doc.setAutoHangingIndentAsync(args?.section_index, args?.paragraph_index, 10 // fallback font size
2709
+ );
2710
+ if (indentPt === 0) {
2711
+ return success({ message: 'No marker detected in paragraph text. No hanging indent applied.', indent_pt: 0 });
2712
+ }
2713
+ return success({ message: `Auto hanging indent applied: ${indentPt.toFixed(2)}pt`, indent_pt: indentPt });
2714
+ }
2715
+ case 'set_table_cell_auto_hanging_indent': {
2716
+ const doc = getDoc(args?.doc_id);
2717
+ if (!doc)
2718
+ return error('Document not found');
2719
+ if (doc.format === 'hwp')
2720
+ return error('HWP files are read-only');
2721
+ // Use async version to read actual font size from document
2722
+ const fontSizeArg = args?.font_size;
2723
+ const indentPt = fontSizeArg !== undefined
2724
+ ? doc.setTableCellAutoHangingIndent(args?.section_index, args?.table_index, args?.row, args?.col, args?.paragraph_index, fontSizeArg)
2725
+ : await doc.setTableCellAutoHangingIndentAsync(args?.section_index, args?.table_index, args?.row, args?.col, args?.paragraph_index, 10 // fallback font size
2726
+ );
2727
+ if (indentPt === 0) {
2728
+ return success({ message: 'No marker detected in cell text. No hanging indent applied.', indent_pt: 0 });
2729
+ }
2730
+ return success({ message: `Auto hanging indent applied to cell: ${indentPt.toFixed(2)}pt`, indent_pt: indentPt });
2731
+ }
2732
+ // === Search & Replace ===
2733
+ case 'search_text': {
2734
+ const doc = getDoc(args?.doc_id);
2735
+ if (!doc)
2736
+ return error('Document not found');
2737
+ const results = doc.searchText(args?.query, {
2738
+ caseSensitive: args?.case_sensitive,
2739
+ regex: args?.regex,
2740
+ includeTables: args?.include_tables !== false, // default true
2741
+ });
2742
+ return success({
2743
+ query: args?.query,
2744
+ total_matches: results.reduce((sum, r) => sum + r.count, 0),
2745
+ locations: results,
2746
+ });
2747
+ }
2748
+ case 'replace_text': {
2749
+ const doc = getDoc(args?.doc_id);
2750
+ if (!doc)
2751
+ return error('Document not found');
2752
+ if (doc.format === 'hwp')
2753
+ return error('HWP files are read-only');
2754
+ const count = doc.replaceText(args?.old_text, args?.new_text, {
2755
+ caseSensitive: args?.case_sensitive,
2756
+ regex: args?.regex,
2757
+ replaceAll: args?.replace_all ?? true,
2758
+ });
2759
+ return success({ message: `Replaced ${count} occurrence(s)`, count });
2760
+ }
2761
+ case 'batch_replace': {
2762
+ const doc = getDoc(args?.doc_id);
2763
+ if (!doc)
2764
+ return error('Document not found');
2765
+ if (doc.format === 'hwp')
2766
+ return error('HWP files are read-only');
2767
+ const replacements = args?.replacements;
2768
+ if (!replacements)
2769
+ return error('replacements array is required');
2770
+ const results = [];
2771
+ for (const { old_text, new_text } of replacements) {
2772
+ const count = doc.replaceText(old_text, new_text);
2773
+ results.push({ old_text, new_text, count });
2774
+ }
2775
+ return success({ results });
2776
+ }
2777
+ case 'replace_text_in_cell': {
2778
+ const doc = getDoc(args?.doc_id);
2779
+ if (!doc)
2780
+ return error('Document not found');
2781
+ if (doc.format === 'hwp')
2782
+ return error('HWP files are read-only');
2783
+ const result = doc.replaceTextInCell(args?.section_index, args?.table_index, args?.row, args?.col, args?.old_text, args?.new_text, {
2784
+ caseSensitive: args?.case_sensitive,
2785
+ regex: args?.regex,
2786
+ replaceAll: args?.replace_all ?? true,
2787
+ });
2788
+ if (!result.success) {
2789
+ return error(result.error || 'Replace failed');
2790
+ }
2791
+ return success({
2792
+ message: `Replaced ${result.count} occurrence(s) in cell [${args?.row}, ${args?.col}]`,
2793
+ count: result.count,
2794
+ });
2795
+ }
2796
+ // === Table Operations ===
2797
+ case 'get_tables': {
2798
+ const doc = getDoc(args?.doc_id);
2799
+ if (!doc)
2800
+ return error('Document not found');
2801
+ return success({ tables: doc.getTables() });
2802
+ }
2803
+ case 'get_table_map': {
2804
+ const doc = getDoc(args?.doc_id);
2805
+ if (!doc)
2806
+ return error('Document not found');
2807
+ return success({ table_map: doc.getTableMap() });
2808
+ }
2809
+ case 'find_empty_tables': {
2810
+ const doc = getDoc(args?.doc_id);
2811
+ if (!doc)
2812
+ return error('Document not found');
2813
+ return success({ empty_tables: doc.findEmptyTables() });
2814
+ }
2815
+ case 'get_tables_by_section': {
2816
+ const doc = getDoc(args?.doc_id);
2817
+ if (!doc)
2818
+ return error('Document not found');
2819
+ const sectionIndex = args?.section_index;
2820
+ if (typeof sectionIndex !== 'number')
2821
+ return error('section_index is required');
2822
+ return success({ tables: doc.getTablesBySection(sectionIndex) });
2823
+ }
2824
+ case 'find_table_by_header': {
2825
+ const doc = getDoc(args?.doc_id);
2826
+ if (!doc)
2827
+ return error('Document not found');
2828
+ const searchText = args?.search_text;
2829
+ if (!searchText)
2830
+ return error('search_text is required');
2831
+ return success({ tables: doc.findTableByHeader(searchText) });
2832
+ }
2833
+ case 'get_tables_summary': {
2834
+ const doc = getDoc(args?.doc_id);
2835
+ if (!doc)
2836
+ return error('Document not found');
2837
+ const startIndex = args?.start_index;
2838
+ const endIndex = args?.end_index;
2839
+ return success({ tables: doc.getTablesSummary(startIndex, endIndex) });
2840
+ }
2841
+ case 'get_document_outline': {
2842
+ const doc = getDoc(args?.doc_id);
2843
+ if (!doc)
2844
+ return error('Document not found');
2845
+ return success({ outline: doc.getDocumentOutline() });
2846
+ }
2847
+ // === Position/Index Helper Handlers ===
2848
+ case 'get_element_index_for_table': {
2849
+ const doc = getDoc(args?.doc_id);
2850
+ if (!doc)
2851
+ return error('Document not found');
2852
+ const tableIndex = args?.table_index;
2853
+ if (typeof tableIndex !== 'number')
2854
+ return error('table_index is required');
2855
+ const result = doc.getElementIndexForTable(tableIndex);
2856
+ if (!result)
2857
+ return error(`Table ${tableIndex} not found`);
2858
+ return success(result);
2859
+ }
2860
+ case 'find_paragraph_by_text': {
2861
+ const doc = getDoc(args?.doc_id);
2862
+ if (!doc)
2863
+ return error('Document not found');
2864
+ const searchText = args?.search_text;
2865
+ if (!searchText)
2866
+ return error('search_text is required');
2867
+ const sectionIndex = args?.section_index;
2868
+ const results = doc.findParagraphByText(searchText, sectionIndex);
2869
+ return success({ matches: results, count: results.length });
2870
+ }
2871
+ case 'get_insert_context': {
2872
+ const doc = getDoc(args?.doc_id);
2873
+ if (!doc)
2874
+ return error('Document not found');
2875
+ const sectionIdx = args?.section_index;
2876
+ const elementIdx = args?.element_index;
2877
+ if (typeof sectionIdx !== 'number')
2878
+ return error('section_index is required');
2879
+ if (typeof elementIdx !== 'number')
2880
+ return error('element_index is required');
2881
+ const contextRange = args?.context_range;
2882
+ const result = doc.getInsertContext(sectionIdx, elementIdx, contextRange);
2883
+ if (!result)
2884
+ return error('Invalid section or element index');
2885
+ return success(result);
2886
+ }
2887
+ case 'find_insert_position_after_header': {
2888
+ const doc = getDoc(args?.doc_id);
2889
+ if (!doc)
2890
+ return error('Document not found');
2891
+ const headerText = args?.header_text;
2892
+ if (!headerText)
2893
+ return error('header_text is required');
2894
+ const searchIn = args?.search_in || 'all';
2895
+ const result = doc.findInsertPositionAfterHeader(headerText, searchIn);
2896
+ if (!result)
2897
+ return error(`Text "${headerText}" not found in ${searchIn === 'all' ? 'paragraphs or table cells' : searchIn}`);
2898
+ return success({
2899
+ ...result,
2900
+ usage_hint: result.found_in === 'table_cell'
2901
+ ? `Found in table cell. Use section_index=${result.section_index} and after_index=${result.insert_after} to insert AFTER this table, or use insert_image_in_cell with table_index=${result.table_info?.table_index}, row=${result.table_info?.row}, col=${result.table_info?.col} to insert INSIDE this cell.`
2902
+ : `Use section_index=${result.section_index} and after_index=${result.insert_after} in insert_image/render_mermaid`,
2903
+ });
2904
+ }
2905
+ case 'find_insert_position_after_table': {
2906
+ const doc = getDoc(args?.doc_id);
2907
+ if (!doc)
2908
+ return error('Document not found');
2909
+ const tableIndex = args?.table_index;
2910
+ if (typeof tableIndex !== 'number')
2911
+ return error('table_index is required');
2912
+ const result = doc.findInsertPositionAfterTable(tableIndex);
2913
+ if (!result)
2914
+ return error(`Table ${tableIndex} not found`);
2915
+ return success({
2916
+ ...result,
2917
+ usage_hint: `Use section_index=${result.section_index} and after_index=${result.insert_after} in insert_image/render_mermaid`,
2918
+ });
2919
+ }
2920
+ case 'get_table': {
2921
+ const doc = getDoc(args?.doc_id);
2922
+ if (!doc)
2923
+ return error('Document not found');
2924
+ const table = doc.getTable(args?.section_index, args?.table_index);
2925
+ if (!table)
2926
+ return error('Table not found');
2927
+ return success(table);
2928
+ }
2929
+ case 'get_table_cell': {
2930
+ const doc = getDoc(args?.doc_id);
2931
+ if (!doc)
2932
+ return error('Document not found');
2933
+ const cell = doc.getTableCell(args?.section_index, args?.table_index, args?.row, args?.col);
2934
+ if (!cell)
2935
+ return error('Cell not found');
2936
+ return success(cell);
2937
+ }
2938
+ case 'update_table_cell': {
2939
+ const docId = args?.doc_id;
2940
+ const doc = getDoc(docId);
2941
+ if (!doc)
2942
+ return error('Document not found');
2943
+ if (doc.format === 'hwp')
2944
+ return error('HWP files are read-only');
2945
+ // Use document lock to prevent race conditions during parallel updates
2946
+ return await withDocumentLock(docId, async () => {
2947
+ const sectionIndex = args?.section_index;
2948
+ const tableIndex = args?.table_index;
2949
+ const row = args?.row;
2950
+ const col = args?.col;
2951
+ const charShapeId = args?.char_shape_id;
2952
+ if (!doc.updateTableCell(sectionIndex, tableIndex, row, col, args?.text, charShapeId)) {
2953
+ return error('Failed to update cell');
2954
+ }
2955
+ // Auto hanging indent (default: true)
2956
+ // Apply to ALL lines in the text, not just the first paragraph
2957
+ const autoHangingIndent = args?.auto_hanging_indent !== false;
2958
+ const appliedIndents = [];
2959
+ if (autoHangingIndent) {
2960
+ const text = args?.text;
2961
+ const lines = text.split('\n');
2962
+ const calculator = new HangingIndentCalculator_1.HangingIndentCalculator();
2963
+ // Apply hanging indent to each line that has a marker
2964
+ for (let i = 0; i < lines.length; i++) {
2965
+ const lineText = lines[i];
2966
+ const indentPt = calculator.calculateHangingIndent(lineText, 10);
2967
+ if (indentPt > 0) {
2968
+ // Register hanging indent for this paragraph
2969
+ // This will be applied when save() is called
2970
+ doc.setTableCellHangingIndent(sectionIndex, tableIndex, row, col, i, indentPt);
2971
+ appliedIndents.push(indentPt);
2972
+ }
2973
+ }
2974
+ }
2975
+ if (appliedIndents.length > 0) {
2976
+ return success({
2977
+ message: `Cell updated with hanging indent applied to ${appliedIndents.length} paragraph(s)`,
2978
+ indent_pts: appliedIndents
2979
+ });
2980
+ }
2981
+ return success({ message: 'Cell updated' });
2982
+ });
2983
+ }
2984
+ case 'find_cell_by_label': {
2985
+ const doc = getDoc(args?.doc_id);
2986
+ if (!doc)
2987
+ return error('Document not found');
2988
+ const results = doc.findCellByLabel(args?.label_text, args?.direction);
2989
+ return success({ matches: results, count: results.length });
2990
+ }
2991
+ case 'fill_by_path': {
2992
+ const docId = args?.doc_id;
2993
+ const doc = getDoc(docId);
2994
+ if (!doc)
2995
+ return error('Document not found');
2996
+ if (doc.format === 'hwp')
2997
+ return error('HWP files are read-only');
2998
+ // Use document lock to prevent race conditions
2999
+ return await withDocumentLock(docId, async () => {
3000
+ const result = doc.fillByPath(args?.mappings);
3001
+ return success(result);
3002
+ });
3003
+ }
3004
+ case 'get_cell_context': {
3005
+ const doc = getDoc(args?.doc_id);
3006
+ if (!doc)
3007
+ return error('Document not found');
3008
+ const globalIdx = args?.table_index;
3009
+ const location = doc.convertGlobalToLocalTableIndex(globalIdx);
3010
+ if (!location) {
3011
+ return error(`Table with global index ${globalIdx} not found`);
3012
+ }
3013
+ const context = doc.getCellContext(globalIdx, args?.row, args?.col, args?.depth);
3014
+ if (!context) {
3015
+ return error('Failed to get cell context');
3016
+ }
3017
+ return success(context);
3018
+ }
3019
+ case 'batch_fill_table': {
3020
+ const docId = args?.doc_id;
3021
+ const doc = getDoc(docId);
3022
+ if (!doc)
3023
+ return error('Document not found');
3024
+ if (doc.format === 'hwp')
3025
+ return error('HWP files are read-only');
3026
+ // Use document lock to prevent race conditions
3027
+ return await withDocumentLock(docId, async () => {
3028
+ const globalIdx = args?.table_index;
3029
+ const location = doc.convertGlobalToLocalTableIndex(globalIdx);
3030
+ if (!location) {
3031
+ return error(`Table with global index ${globalIdx} not found`);
3032
+ }
3033
+ const result = doc.batchFillTable(globalIdx, args?.data, args?.start_row, args?.start_col);
3034
+ return success(result);
3035
+ });
3036
+ }
3037
+ case 'set_cell_properties': {
3038
+ const doc = getDoc(args?.doc_id);
3039
+ if (!doc)
3040
+ return error('Document not found');
3041
+ if (doc.format === 'hwp')
3042
+ return error('HWP files are read-only');
3043
+ const props = {};
3044
+ if (args?.width)
3045
+ props.width = args.width;
3046
+ if (args?.height)
3047
+ props.height = args.height;
3048
+ if (args?.background_color)
3049
+ props.backgroundColor = args.background_color;
3050
+ if (args?.vertical_align)
3051
+ props.verticalAlign = args.vertical_align;
3052
+ if (doc.setCellProperties(args?.section_index, args?.table_index, args?.row, args?.col, props)) {
3053
+ return success({ message: 'Cell properties updated' });
3054
+ }
3055
+ return error('Failed to update cell properties');
3056
+ }
3057
+ case 'merge_cells': {
3058
+ const doc = getDoc(args?.doc_id);
3059
+ if (!doc)
3060
+ return error('Document not found');
3061
+ if (doc.format === 'hwp')
3062
+ return error('HWP files are read-only');
3063
+ if (doc.mergeCells(args?.section_index, args?.table_index, args?.start_row, args?.start_col, args?.end_row, args?.end_col)) {
3064
+ const colSpan = args?.end_col - args?.start_col + 1;
3065
+ const rowSpan = args?.end_row - args?.start_row + 1;
3066
+ return success({
3067
+ message: `Cells merged successfully`,
3068
+ colSpan,
3069
+ rowSpan,
3070
+ masterCell: { row: args?.start_row, col: args?.start_col }
3071
+ });
3072
+ }
3073
+ return error('Failed to merge cells. Check that the range is valid and cells are not already merged.');
3074
+ }
3075
+ case 'split_cell': {
3076
+ const doc = getDoc(args?.doc_id);
3077
+ if (!doc)
3078
+ return error('Document not found');
3079
+ if (doc.format === 'hwp')
3080
+ return error('HWP files are read-only');
3081
+ if (doc.splitCell(args?.section_index, args?.table_index, args?.row, args?.col)) {
3082
+ return success({
3083
+ message: `Cell split successfully`,
3084
+ cell: { row: args?.row, col: args?.col }
3085
+ });
3086
+ }
3087
+ return error('Failed to split cell. Check that the cell is actually merged (colSpan > 1 or rowSpan > 1).');
3088
+ }
3089
+ case 'insert_table_row': {
3090
+ const doc = getDoc(args?.doc_id);
3091
+ if (!doc)
3092
+ return error('Document not found');
3093
+ if (doc.format === 'hwp')
3094
+ return error('HWP files are read-only');
3095
+ if (doc.insertTableRow(args?.section_index, args?.table_index, args?.after_row, args?.cell_texts)) {
3096
+ return success({ message: 'Row inserted' });
3097
+ }
3098
+ return error('Failed to insert row');
3099
+ }
3100
+ case 'delete_table': {
3101
+ const doc = getDoc(args?.doc_id);
3102
+ if (!doc)
3103
+ return error('Document not found');
3104
+ if (doc.format === 'hwp')
3105
+ return error('HWP files are read-only');
3106
+ if (doc.deleteTable(args?.section_index, args?.table_index)) {
3107
+ return success({ message: 'Table deleted' });
3108
+ }
3109
+ return error('Failed to delete table');
3110
+ }
3111
+ case 'delete_table_row': {
3112
+ const doc = getDoc(args?.doc_id);
3113
+ if (!doc)
3114
+ return error('Document not found');
3115
+ if (doc.format === 'hwp')
3116
+ return error('HWP files are read-only');
3117
+ const sectionIndex = args?.section_index;
3118
+ const tableIndex = args?.table_index;
3119
+ const allTables = doc.getTables();
3120
+ const table = allTables.find(t => t.section === sectionIndex && t.index === tableIndex);
3121
+ const wasOnlyRow = table && table.rows === 1;
3122
+ if (doc.deleteTableRow(args?.section_index, args?.table_index, args?.row_index)) {
3123
+ if (wasOnlyRow) {
3124
+ return success({ message: 'Table deleted (was only row)' });
3125
+ }
3126
+ return success({ message: 'Row deleted' });
3127
+ }
3128
+ return error('Failed to delete row');
3129
+ }
3130
+ case 'insert_table_column': {
3131
+ const doc = getDoc(args?.doc_id);
3132
+ if (!doc)
3133
+ return error('Document not found');
3134
+ if (doc.format === 'hwp')
3135
+ return error('HWP files are read-only');
3136
+ if (doc.insertTableColumn(args?.section_index, args?.table_index, args?.after_col)) {
3137
+ return success({ message: 'Column inserted' });
3138
+ }
3139
+ return error('Failed to insert column');
3140
+ }
3141
+ case 'delete_table_column': {
3142
+ const doc = getDoc(args?.doc_id);
3143
+ if (!doc)
3144
+ return error('Document not found');
3145
+ if (doc.format === 'hwp')
3146
+ return error('HWP files are read-only');
3147
+ if (doc.deleteTableColumn(args?.section_index, args?.table_index, args?.col_index)) {
3148
+ return success({ message: 'Column deleted' });
3149
+ }
3150
+ return error('Failed to delete column');
3151
+ }
3152
+ case 'get_table_as_csv': {
3153
+ const doc = getDoc(args?.doc_id);
3154
+ if (!doc)
3155
+ return error('Document not found');
3156
+ const csv = doc.getTableAsCsv(args?.section_index, args?.table_index, args?.delimiter || ',');
3157
+ if (!csv)
3158
+ return error('Table not found');
3159
+ return success({ csv });
3160
+ }
3161
+ // === Page Settings ===
3162
+ case 'get_page_settings': {
3163
+ const doc = getDoc(args?.doc_id);
3164
+ if (!doc)
3165
+ return error('Document not found');
3166
+ const settings = doc.getPageSettings(args?.section_index || 0);
3167
+ return success({ settings });
3168
+ }
3169
+ case 'set_page_settings': {
3170
+ const doc = getDoc(args?.doc_id);
3171
+ if (!doc)
3172
+ return error('Document not found');
3173
+ if (doc.format === 'hwp')
3174
+ return error('HWP files are read-only');
3175
+ const settings = {};
3176
+ if (args?.width)
3177
+ settings.width = args.width;
3178
+ if (args?.height)
3179
+ settings.height = args.height;
3180
+ if (args?.margin_top)
3181
+ settings.marginTop = args.margin_top;
3182
+ if (args?.margin_bottom)
3183
+ settings.marginBottom = args.margin_bottom;
3184
+ if (args?.margin_left)
3185
+ settings.marginLeft = args.margin_left;
3186
+ if (args?.margin_right)
3187
+ settings.marginRight = args.margin_right;
3188
+ if (args?.orientation)
3189
+ settings.orientation = args.orientation;
3190
+ if (doc.setPageSettings(args?.section_index || 0, settings)) {
3191
+ return success({ message: 'Page settings updated' });
3192
+ }
3193
+ return error('Failed to update page settings');
3194
+ }
3195
+ // === Copy/Move ===
3196
+ case 'copy_paragraph': {
3197
+ const doc = getDoc(args?.doc_id);
3198
+ if (!doc)
3199
+ return error('Document not found');
3200
+ if (doc.format === 'hwp')
3201
+ return error('HWP files are read-only');
3202
+ if (doc.copyParagraph(args?.source_section, args?.source_paragraph, args?.target_section, args?.target_after)) {
3203
+ return success({ message: 'Paragraph copied' });
3204
+ }
3205
+ return error('Failed to copy paragraph');
3206
+ }
3207
+ case 'move_paragraph': {
3208
+ const doc = getDoc(args?.doc_id);
3209
+ if (!doc)
3210
+ return error('Document not found');
3211
+ if (doc.format === 'hwp')
3212
+ return error('HWP files are read-only');
3213
+ if (doc.moveParagraph(args?.source_section, args?.source_paragraph, args?.target_section, args?.target_after)) {
3214
+ return success({ message: 'Paragraph moved' });
3215
+ }
3216
+ return error('Failed to move paragraph');
3217
+ }
3218
+ case 'move_table': {
3219
+ const doc = getDoc(args?.doc_id);
3220
+ if (!doc)
3221
+ return error('Document not found');
3222
+ if (doc.format === 'hwp')
3223
+ return error('HWP files are read-only');
3224
+ const result = doc.moveTable(args?.section_index, args?.table_index, args?.target_section_index, args?.target_after_index);
3225
+ if (result.success) {
3226
+ return success({ message: 'Table move scheduled. Changes will be applied on save.' });
3227
+ }
3228
+ return error(result.error || 'Failed to move table');
3229
+ }
3230
+ case 'copy_table': {
3231
+ const doc = getDoc(args?.doc_id);
3232
+ if (!doc)
3233
+ return error('Document not found');
3234
+ if (doc.format === 'hwp')
3235
+ return error('HWP files are read-only');
3236
+ const result = doc.copyTable(args?.section_index, args?.table_index, args?.target_section_index, args?.target_after_index);
3237
+ if (result.success) {
3238
+ return success({ message: 'Table copy scheduled. Changes will be applied on save.' });
3239
+ }
3240
+ return error(result.error || 'Failed to copy table');
3241
+ }
3242
+ // === Statistics ===
3243
+ case 'get_word_count': {
3244
+ const doc = getDoc(args?.doc_id);
3245
+ if (!doc)
3246
+ return error('Document not found');
3247
+ return success(doc.getWordCount());
3248
+ }
3249
+ // === Images ===
3250
+ case 'get_images': {
3251
+ const doc = getDoc(args?.doc_id);
3252
+ if (!doc)
3253
+ return error('Document not found');
3254
+ return success({ images: doc.getImages() });
3255
+ }
3256
+ // === Export ===
3257
+ case 'export_to_text': {
3258
+ const doc = getDoc(args?.doc_id);
3259
+ if (!doc)
3260
+ return error('Document not found');
3261
+ const text = doc.getAllText();
3262
+ const outputPath = args?.output_path;
3263
+ fs.writeFileSync(outputPath, text, 'utf-8');
3264
+ return success({ message: `Exported to ${outputPath}`, characters: text.length });
3265
+ }
3266
+ case 'export_to_html': {
3267
+ const doc = getDoc(args?.doc_id);
3268
+ if (!doc)
3269
+ return error('Document not found');
3270
+ let html = '<!DOCTYPE html><html><head><meta charset="UTF-8">';
3271
+ html += '<style>body{font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px;}table{border-collapse:collapse;width:100%;}td,th{border:1px solid #ccc;padding:8px;}</style>';
3272
+ html += '</head><body>';
3273
+ const content = doc.content;
3274
+ for (const section of content.sections) {
3275
+ for (const element of section.elements) {
3276
+ if (element.type === 'paragraph') {
3277
+ const text = element.data.runs.map(r => escapeHtml(r.text)).join('');
3278
+ html += `<p>${text}</p>`;
3279
+ }
3280
+ else if (element.type === 'table') {
3281
+ const table = element.data;
3282
+ html += '<table>';
3283
+ for (const row of table.rows) {
3284
+ html += '<tr>';
3285
+ for (const cell of row.cells) {
3286
+ const text = cell.paragraphs.map(p => p.runs.map(r => escapeHtml(r.text)).join('')).join('<br>');
3287
+ html += `<td>${text}</td>`;
3288
+ }
3289
+ html += '</tr>';
3290
+ }
3291
+ html += '</table>';
3292
+ }
3293
+ }
3294
+ }
3295
+ html += '</body></html>';
3296
+ const outputPath = args?.output_path;
3297
+ fs.writeFileSync(outputPath, html, 'utf-8');
3298
+ return success({ message: `Exported to ${outputPath}` });
3299
+ }
3300
+ // === Undo/Redo ===
3301
+ case 'undo': {
3302
+ const doc = getDoc(args?.doc_id);
3303
+ if (!doc)
3304
+ return error('Document not found');
3305
+ const count = args?.count || 1;
3306
+ let undoneCount = 0;
3307
+ for (let i = 0; i < count; i++) {
3308
+ if (doc.undo()) {
3309
+ undoneCount++;
3310
+ }
3311
+ else {
3312
+ break;
3313
+ }
3314
+ }
3315
+ if (undoneCount > 0) {
3316
+ return success({
3317
+ message: `Undo successful (${undoneCount}/${count})`,
3318
+ undone_count: undoneCount,
3319
+ canUndo: doc.canUndo(),
3320
+ canRedo: doc.canRedo()
3321
+ });
3322
+ }
3323
+ return error('Nothing to undo');
3324
+ }
3325
+ case 'redo': {
3326
+ const doc = getDoc(args?.doc_id);
3327
+ if (!doc)
3328
+ return error('Document not found');
3329
+ const count = args?.count || 1;
3330
+ let redoneCount = 0;
3331
+ for (let i = 0; i < count; i++) {
3332
+ if (doc.redo()) {
3333
+ redoneCount++;
3334
+ }
3335
+ else {
3336
+ break;
3337
+ }
3338
+ }
3339
+ if (redoneCount > 0) {
3340
+ return success({
3341
+ message: `Redo successful (${redoneCount}/${count})`,
3342
+ redone_count: redoneCount,
3343
+ canUndo: doc.canUndo(),
3344
+ canRedo: doc.canRedo()
3345
+ });
3346
+ }
3347
+ return error('Nothing to redo');
3348
+ }
3349
+ // === Table Creation ===
3350
+ case 'insert_table': {
3351
+ const doc = getDoc(args?.doc_id);
3352
+ if (!doc)
3353
+ return error('Document not found');
3354
+ if (doc.format === 'hwp')
3355
+ return error('HWP files are read-only');
3356
+ const result = doc.insertTable(args?.section_index, args?.after_index, args?.rows, args?.cols, { width: args?.width });
3357
+ if (!result)
3358
+ return error('Failed to insert table');
3359
+ return success({ message: 'Table inserted', tableIndex: result.tableIndex });
3360
+ }
3361
+ case 'insert_nested_table': {
3362
+ const doc = getDoc(args?.doc_id);
3363
+ if (!doc)
3364
+ return error('Document not found');
3365
+ if (doc.format === 'hwp')
3366
+ return error('HWP files are read-only');
3367
+ const result = doc.insertNestedTable(args?.section_index, args?.parent_table_index, args?.row, args?.col, args?.nested_rows, args?.nested_cols, { data: args?.data });
3368
+ if (!result.success)
3369
+ return error(result.error || 'Failed to insert nested table');
3370
+ return success({ message: 'Nested table inserted successfully' });
3371
+ }
3372
+ // === Header/Footer ===
3373
+ case 'get_header': {
3374
+ const doc = getDoc(args?.doc_id);
3375
+ if (!doc)
3376
+ return error('Document not found');
3377
+ const result = doc.getHeader(args?.section_index || 0);
3378
+ return success({ header: result });
3379
+ }
3380
+ case 'set_header': {
3381
+ const doc = getDoc(args?.doc_id);
3382
+ if (!doc)
3383
+ return error('Document not found');
3384
+ if (doc.format === 'hwp')
3385
+ return error('HWP files are read-only');
3386
+ if (doc.setHeader(args?.section_index || 0, args?.text)) {
3387
+ return success({ message: 'Header set successfully' });
3388
+ }
3389
+ return error('Failed to set header');
3390
+ }
3391
+ case 'get_footer': {
3392
+ const doc = getDoc(args?.doc_id);
3393
+ if (!doc)
3394
+ return error('Document not found');
3395
+ const result = doc.getFooter(args?.section_index || 0);
3396
+ return success({ footer: result });
3397
+ }
3398
+ case 'set_footer': {
3399
+ const doc = getDoc(args?.doc_id);
3400
+ if (!doc)
3401
+ return error('Document not found');
3402
+ if (doc.format === 'hwp')
3403
+ return error('HWP files are read-only');
3404
+ if (doc.setFooter(args?.section_index || 0, args?.text)) {
3405
+ return success({ message: 'Footer set successfully' });
3406
+ }
3407
+ return error('Failed to set footer');
3408
+ }
3409
+ // === Footnotes/Endnotes ===
3410
+ case 'get_footnotes': {
3411
+ const doc = getDoc(args?.doc_id);
3412
+ if (!doc)
3413
+ return error('Document not found');
3414
+ return success({ footnotes: doc.getFootnotes() });
3415
+ }
3416
+ case 'insert_footnote': {
3417
+ const doc = getDoc(args?.doc_id);
3418
+ if (!doc)
3419
+ return error('Document not found');
3420
+ if (doc.format === 'hwp')
3421
+ return error('HWP files are read-only');
3422
+ const result = doc.insertFootnote(args?.section_index, args?.paragraph_index, args?.text);
3423
+ if (!result)
3424
+ return error('Failed to insert footnote');
3425
+ return success({ message: 'Footnote inserted', id: result.id });
3426
+ }
3427
+ case 'get_endnotes': {
3428
+ const doc = getDoc(args?.doc_id);
3429
+ if (!doc)
3430
+ return error('Document not found');
3431
+ return success({ endnotes: doc.getEndnotes() });
3432
+ }
3433
+ case 'insert_endnote': {
3434
+ const doc = getDoc(args?.doc_id);
3435
+ if (!doc)
3436
+ return error('Document not found');
3437
+ if (doc.format === 'hwp')
3438
+ return error('HWP files are read-only');
3439
+ const result = doc.insertEndnote(args?.section_index, args?.paragraph_index, args?.text);
3440
+ if (!result)
3441
+ return error('Failed to insert endnote');
3442
+ return success({ message: 'Endnote inserted', id: result.id });
3443
+ }
3444
+ // === Bookmarks/Hyperlinks ===
3445
+ case 'get_bookmarks': {
3446
+ const doc = getDoc(args?.doc_id);
3447
+ if (!doc)
3448
+ return error('Document not found');
3449
+ return success({ bookmarks: doc.getBookmarks() });
3450
+ }
3451
+ case 'insert_bookmark': {
3452
+ const doc = getDoc(args?.doc_id);
3453
+ if (!doc)
3454
+ return error('Document not found');
3455
+ if (doc.format === 'hwp')
3456
+ return error('HWP files are read-only');
3457
+ if (doc.insertBookmark(args?.section_index, args?.paragraph_index, args?.name)) {
3458
+ return success({ message: 'Bookmark inserted' });
3459
+ }
3460
+ return error('Failed to insert bookmark');
3461
+ }
3462
+ case 'get_hyperlinks': {
3463
+ const doc = getDoc(args?.doc_id);
3464
+ if (!doc)
3465
+ return error('Document not found');
3466
+ return success({ hyperlinks: doc.getHyperlinks() });
3467
+ }
3468
+ case 'insert_hyperlink': {
3469
+ const doc = getDoc(args?.doc_id);
3470
+ if (!doc)
3471
+ return error('Document not found');
3472
+ if (doc.format === 'hwp')
3473
+ return error('HWP files are read-only');
3474
+ if (doc.insertHyperlink(args?.section_index, args?.paragraph_index, args?.url, args?.text)) {
3475
+ return success({ message: 'Hyperlink inserted' });
3476
+ }
3477
+ return error('Failed to insert hyperlink');
3478
+ }
3479
+ // === Image Operations ===
3480
+ case 'insert_image': {
3481
+ const doc = getDoc(args?.doc_id);
3482
+ if (!doc)
3483
+ return error('Document not found');
3484
+ if (doc.format === 'hwp')
3485
+ return error('HWP files are read-only');
3486
+ const imagePath = args?.image_path;
3487
+ if (!fs.existsSync(imagePath))
3488
+ return error('Image file not found');
3489
+ // Resolve position using after_table, after_header, or direct indices
3490
+ let sectionIndex = args?.section_index;
3491
+ let afterIndex = args?.after_index;
3492
+ let insertedAfter = '';
3493
+ const afterTable = args?.after_table;
3494
+ const afterHeader = args?.after_header;
3495
+ if (afterTable !== undefined) {
3496
+ // Insert after a specific table
3497
+ const pos = doc.findInsertPositionAfterTable(afterTable);
3498
+ if (!pos)
3499
+ return error(`Table ${afterTable} not found`);
3500
+ sectionIndex = pos.section_index;
3501
+ afterIndex = pos.insert_after;
3502
+ insertedAfter = `table ${afterTable} ("${pos.table_info.header.substring(0, 50)}")`;
3503
+ }
3504
+ else if (afterHeader) {
3505
+ // Insert after a header paragraph
3506
+ const pos = doc.findInsertPositionAfterHeader(afterHeader);
3507
+ if (!pos)
3508
+ return error(`Header "${afterHeader}" not found`);
3509
+ sectionIndex = pos.section_index;
3510
+ afterIndex = pos.insert_after;
3511
+ insertedAfter = `header "${pos.header_found.substring(0, 50)}"`;
3512
+ }
3513
+ else {
3514
+ // Use direct indices
3515
+ if (sectionIndex === undefined)
3516
+ return error('section_index is required when not using after_table or after_header');
3517
+ if (afterIndex === undefined)
3518
+ return error('after_index is required when not using after_table or after_header');
3519
+ insertedAfter = `element ${afterIndex}`;
3520
+ }
3521
+ const imageData = fs.readFileSync(imagePath);
3522
+ const ext = path.extname(imagePath).toLowerCase();
3523
+ const mimeTypes = {
3524
+ '.png': 'image/png',
3525
+ '.jpg': 'image/jpeg',
3526
+ '.jpeg': 'image/jpeg',
3527
+ '.gif': 'image/gif',
3528
+ '.bmp': 'image/bmp',
3529
+ };
3530
+ const preserveAspectRatio = args?.preserve_aspect_ratio;
3531
+ // Build position options from args
3532
+ const position = (args?.position_type || args?.vert_rel_to || args?.horz_rel_to ||
3533
+ args?.vert_align || args?.horz_align || args?.vert_offset !== undefined ||
3534
+ args?.horz_offset !== undefined || args?.text_wrap) ? {
3535
+ positionType: args?.position_type,
3536
+ vertRelTo: args?.vert_rel_to,
3537
+ horzRelTo: args?.horz_rel_to,
3538
+ vertAlign: args?.vert_align,
3539
+ horzAlign: args?.horz_align,
3540
+ vertOffset: args?.vert_offset,
3541
+ horzOffset: args?.horz_offset,
3542
+ textWrap: args?.text_wrap,
3543
+ } : undefined;
3544
+ const result = doc.insertImage(sectionIndex, afterIndex, {
3545
+ data: imageData.toString('base64'),
3546
+ mimeType: mimeTypes[ext] || 'image/png',
3547
+ width: args?.width,
3548
+ height: args?.height,
3549
+ preserveAspectRatio,
3550
+ position,
3551
+ headerText: afterHeader, // Pass header text for precise XML positioning
3552
+ });
3553
+ if (!result)
3554
+ return error('Failed to insert image');
3555
+ // Get context around insertion point for verification
3556
+ const context = doc.getInsertContext(sectionIndex, afterIndex + 1, 1);
3557
+ return success({
3558
+ message: `Image inserted after ${insertedAfter}`,
3559
+ id: result.id,
3560
+ actualWidth: result.actualWidth,
3561
+ actualHeight: result.actualHeight,
3562
+ section_index: sectionIndex,
3563
+ element_index: afterIndex + 1,
3564
+ context: context ? {
3565
+ before: context.elements_before.map(e => e.text).join(' → '),
3566
+ after: context.elements_after.map(e => e.text).join(' → '),
3567
+ } : undefined,
3568
+ });
3569
+ }
3570
+ case 'update_image_size': {
3571
+ const doc = getDoc(args?.doc_id);
3572
+ if (!doc)
3573
+ return error('Document not found');
3574
+ if (doc.format === 'hwp')
3575
+ return error('HWP files are read-only');
3576
+ // Find image ID from section and index
3577
+ const images = doc.getImages();
3578
+ const imageIndex = args?.image_index;
3579
+ if (imageIndex < 0 || imageIndex >= images.length)
3580
+ return error('Image not found');
3581
+ if (doc.updateImageSize(images[imageIndex].id, args?.width, args?.height)) {
3582
+ return success({ message: 'Image size updated' });
3583
+ }
3584
+ return error('Failed to update image size');
3585
+ }
3586
+ case 'delete_image': {
3587
+ const doc = getDoc(args?.doc_id);
3588
+ if (!doc)
3589
+ return error('Document not found');
3590
+ if (doc.format === 'hwp')
3591
+ return error('HWP files are read-only');
3592
+ const images = doc.getImages();
3593
+ const imageIndex = args?.image_index;
3594
+ if (imageIndex < 0 || imageIndex >= images.length)
3595
+ return error('Image not found');
3596
+ if (doc.deleteImage(images[imageIndex].id)) {
3597
+ return success({ message: 'Image deleted' });
3598
+ }
3599
+ return error('Failed to delete image');
3600
+ }
3601
+ case 'render_mermaid': {
3602
+ const doc = getDoc(args?.doc_id);
3603
+ if (!doc)
3604
+ return error('Document not found');
3605
+ if (doc.format === 'hwp')
3606
+ return error('HWP files are read-only');
3607
+ const mermaidCode = args?.mermaid_code;
3608
+ if (!mermaidCode)
3609
+ return error('Mermaid code is required');
3610
+ // Resolve position using after_table, after_header, or direct indices
3611
+ let sectionIndex = args?.section_index;
3612
+ let afterIndex = args?.after_index;
3613
+ let insertedAfter = '';
3614
+ const afterTable = args?.after_table;
3615
+ const afterHeader = args?.after_header;
3616
+ if (afterTable !== undefined) {
3617
+ // Insert after a specific table
3618
+ const pos = doc.findInsertPositionAfterTable(afterTable);
3619
+ if (!pos)
3620
+ return error(`Table ${afterTable} not found`);
3621
+ sectionIndex = pos.section_index;
3622
+ afterIndex = pos.insert_after;
3623
+ insertedAfter = `table ${afterTable} ("${pos.table_info.header.substring(0, 50)}")`;
3624
+ }
3625
+ else if (afterHeader) {
3626
+ // Insert after a header paragraph
3627
+ const pos = doc.findInsertPositionAfterHeader(afterHeader);
3628
+ if (!pos)
3629
+ return error(`Header "${afterHeader}" not found`);
3630
+ sectionIndex = pos.section_index;
3631
+ afterIndex = pos.insert_after;
3632
+ insertedAfter = `header "${pos.header_found.substring(0, 50)}"`;
3633
+ }
3634
+ else {
3635
+ // Use direct indices (default section to 0)
3636
+ sectionIndex = sectionIndex ?? 0;
3637
+ if (afterIndex === undefined)
3638
+ return error('after_index is required when not using after_table or after_header');
3639
+ insertedAfter = `element ${afterIndex}`;
3640
+ }
3641
+ // Build position options from args
3642
+ const positionOptions = (args?.position_type || args?.vert_rel_to || args?.horz_rel_to ||
3643
+ args?.vert_align || args?.horz_align || args?.vert_offset !== undefined ||
3644
+ args?.horz_offset !== undefined || args?.text_wrap) ? {
3645
+ positionType: args?.position_type,
3646
+ vertRelTo: args?.vert_rel_to,
3647
+ horzRelTo: args?.horz_rel_to,
3648
+ vertAlign: args?.vert_align,
3649
+ horzAlign: args?.horz_align,
3650
+ vertOffset: args?.vert_offset,
3651
+ horzOffset: args?.horz_offset,
3652
+ textWrap: args?.text_wrap,
3653
+ } : undefined;
3654
+ const result = await doc.renderMermaidToImage(mermaidCode, sectionIndex, afterIndex, {
3655
+ width: args?.width,
3656
+ height: args?.height,
3657
+ theme: args?.theme,
3658
+ backgroundColor: args?.background_color,
3659
+ preserveAspectRatio: args?.preserve_aspect_ratio,
3660
+ position: positionOptions,
3661
+ headerText: afterHeader, // Pass header text for precise XML positioning
3662
+ });
3663
+ if (result.success) {
3664
+ // Get context around insertion point for verification
3665
+ const context = doc.getInsertContext(sectionIndex, afterIndex + 1, 1);
3666
+ return success({
3667
+ message: `Mermaid diagram inserted after ${insertedAfter}`,
3668
+ image_id: result.imageId,
3669
+ actualWidth: result.actualWidth,
3670
+ actualHeight: result.actualHeight,
3671
+ section_index: sectionIndex,
3672
+ element_index: afterIndex + 1,
3673
+ context: context ? {
3674
+ before: context.elements_before.map(e => e.text).join(' → '),
3675
+ after: context.elements_after.map(e => e.text).join(' → '),
3676
+ } : undefined,
3677
+ });
3678
+ }
3679
+ return error(result.error || 'Failed to render Mermaid diagram');
3680
+ }
3681
+ case 'insert_image_in_cell': {
3682
+ const doc = getDoc(args?.doc_id);
3683
+ if (!doc)
3684
+ return error('Document not found');
3685
+ if (doc.format === 'hwp')
3686
+ return error('HWP files are read-only');
3687
+ const imagePath = args?.image_path;
3688
+ if (!fs.existsSync(imagePath))
3689
+ return error('Image file not found');
3690
+ const globalTblIdx = args?.table_index;
3691
+ const rowIdx = args?.row;
3692
+ const colIdx = args?.col;
3693
+ // Convert global table index to section and local index
3694
+ const tableLocation = doc.convertGlobalToLocalTableIndex(globalTblIdx);
3695
+ if (!tableLocation) {
3696
+ return error(`Table with global index ${globalTblIdx} not found. Use get_table_map to find valid table indices.`);
3697
+ }
3698
+ const { section_index: secIdx, local_index: localTblIdx } = tableLocation;
3699
+ const imageData = fs.readFileSync(imagePath);
3700
+ const ext = path.extname(imagePath).toLowerCase();
3701
+ const mimeTypes = {
3702
+ '.png': 'image/png',
3703
+ '.jpg': 'image/jpeg',
3704
+ '.jpeg': 'image/jpeg',
3705
+ '.gif': 'image/gif',
3706
+ '.bmp': 'image/bmp',
3707
+ };
3708
+ const afterText = args?.after_text;
3709
+ const result = doc.insertImageInCell(secIdx, localTblIdx, rowIdx, colIdx, {
3710
+ data: imageData.toString('base64'),
3711
+ mimeType: mimeTypes[ext] || 'image/png',
3712
+ width: args?.width,
3713
+ height: args?.height,
3714
+ preserveAspectRatio: args?.preserve_aspect_ratio,
3715
+ afterText,
3716
+ });
3717
+ if (!result)
3718
+ return error('Failed to insert image in cell. Check row/col indices.');
3719
+ // Get cell content for context using getTableCell
3720
+ const cellInfo = doc.getTableCell(secIdx, localTblIdx, rowIdx, colIdx);
3721
+ const cellText = cellInfo?.text?.substring(0, 30) || '';
3722
+ const positionInfo = afterText
3723
+ ? `after paragraph containing "${afterText}"`
3724
+ : 'at the beginning';
3725
+ return success({
3726
+ message: `Image inserted in cell [${rowIdx}, ${colIdx}] of table ${globalTblIdx} ${positionInfo}`,
3727
+ id: result.id,
3728
+ actualWidth: result.actualWidth,
3729
+ actualHeight: result.actualHeight,
3730
+ cell_content: cellText || '(empty cell)',
3731
+ });
3732
+ }
3733
+ case 'render_mermaid_in_cell': {
3734
+ const doc = getDoc(args?.doc_id);
3735
+ if (!doc)
3736
+ return error('Document not found');
3737
+ if (doc.format === 'hwp')
3738
+ return error('HWP files are read-only');
3739
+ const mermaidCode = args?.mermaid_code;
3740
+ if (!mermaidCode)
3741
+ return error('Mermaid code is required');
3742
+ const globalTblIdx = args?.table_index;
3743
+ const rowIdx = args?.row;
3744
+ const colIdx = args?.col;
3745
+ // Convert global table index to section and local index
3746
+ const tableLocation = doc.convertGlobalToLocalTableIndex(globalTblIdx);
3747
+ if (!tableLocation) {
3748
+ return error(`Table with global index ${globalTblIdx} not found. Use get_table_map to find valid table indices.`);
3749
+ }
3750
+ const { section_index: secIdx, local_index: localTblIdx } = tableLocation;
3751
+ // Fetch Mermaid diagram from mermaid.ink API using pako compression (same as renderMermaidToImage)
3752
+ const pako = await Promise.resolve().then(() => __importStar(require('pako')));
3753
+ const theme = args?.theme || 'default';
3754
+ const bgColor = args?.background_color;
3755
+ const stateObject = {
3756
+ code: mermaidCode,
3757
+ mermaid: { theme: theme },
3758
+ autoSync: true,
3759
+ updateDiagram: true
3760
+ };
3761
+ const jsonString = JSON.stringify(stateObject);
3762
+ const compressed = pako.deflate(jsonString, { level: 9 });
3763
+ const base64Code = Buffer.from(compressed)
3764
+ .toString('base64')
3765
+ .replace(/\+/g, '-')
3766
+ .replace(/\//g, '_');
3767
+ let url = `https://mermaid.ink/img/pako:${base64Code}?type=png`;
3768
+ if (bgColor) {
3769
+ const bgColorClean = bgColor.replace(/^#/, '');
3770
+ url += `&bgColor=${bgColorClean}`;
3771
+ }
3772
+ try {
3773
+ const response = await fetch(url);
3774
+ if (!response.ok) {
3775
+ return error(`Failed to render Mermaid diagram: ${response.statusText}`);
3776
+ }
3777
+ const imageBuffer = Buffer.from(await response.arrayBuffer());
3778
+ const afterText = args?.after_text;
3779
+ const result = doc.insertImageInCell(secIdx, localTblIdx, rowIdx, colIdx, {
3780
+ data: imageBuffer.toString('base64'),
3781
+ mimeType: 'image/png',
3782
+ width: args?.width,
3783
+ height: args?.height,
3784
+ preserveAspectRatio: args?.preserve_aspect_ratio !== false, // default true for Mermaid
3785
+ afterText,
3786
+ });
3787
+ if (!result)
3788
+ return error('Failed to insert Mermaid diagram in cell. Check row/col indices.');
3789
+ // Get cell content for context using getTableCell
3790
+ const cellInfo = doc.getTableCell(secIdx, localTblIdx, rowIdx, colIdx);
3791
+ const cellText = cellInfo?.text?.substring(0, 30) || '';
3792
+ const positionInfo = afterText
3793
+ ? `after paragraph containing "${afterText}"`
3794
+ : 'at the beginning';
3795
+ return success({
3796
+ message: `Mermaid diagram inserted in cell [${rowIdx}, ${colIdx}] of table ${globalTblIdx} ${positionInfo}`,
3797
+ image_id: result.id,
3798
+ actualWidth: result.actualWidth,
3799
+ actualHeight: result.actualHeight,
3800
+ cell_content: cellText || '(empty cell)',
3801
+ });
3802
+ }
3803
+ catch (err) {
3804
+ const errorMessage = err instanceof Error ? err.message : String(err);
3805
+ return error(`Failed to fetch Mermaid diagram: ${errorMessage}`);
3806
+ }
3807
+ }
3808
+ // === Drawing Objects ===
3809
+ case 'insert_line': {
3810
+ const doc = getDoc(args?.doc_id);
3811
+ if (!doc)
3812
+ return error('Document not found');
3813
+ if (doc.format === 'hwp')
3814
+ return error('HWP files are read-only');
3815
+ const result = doc.insertLine(args?.section_index, args?.x1, args?.y1, args?.x2, args?.y2, {
3816
+ color: args?.stroke_color,
3817
+ width: args?.stroke_width,
3818
+ });
3819
+ if (!result)
3820
+ return error('Failed to insert line');
3821
+ return success({ message: 'Line inserted', id: result.id });
3822
+ }
3823
+ case 'insert_rect': {
3824
+ const doc = getDoc(args?.doc_id);
3825
+ if (!doc)
3826
+ return error('Document not found');
3827
+ if (doc.format === 'hwp')
3828
+ return error('HWP files are read-only');
3829
+ const result = doc.insertRect(args?.section_index, args?.x, args?.y, args?.width, args?.height, {
3830
+ fillColor: args?.fill_color,
3831
+ strokeColor: args?.stroke_color,
3832
+ });
3833
+ if (!result)
3834
+ return error('Failed to insert rectangle');
3835
+ return success({ message: 'Rectangle inserted', id: result.id });
3836
+ }
3837
+ case 'insert_ellipse': {
3838
+ const doc = getDoc(args?.doc_id);
3839
+ if (!doc)
3840
+ return error('Document not found');
3841
+ if (doc.format === 'hwp')
3842
+ return error('HWP files are read-only');
3843
+ const result = doc.insertEllipse(args?.section_index, args?.cx, args?.cy, args?.rx, args?.ry, {
3844
+ fillColor: args?.fill_color,
3845
+ strokeColor: args?.stroke_color,
3846
+ });
3847
+ if (!result)
3848
+ return error('Failed to insert ellipse');
3849
+ return success({ message: 'Ellipse inserted', id: result.id });
3850
+ }
3851
+ // === Equations ===
3852
+ case 'get_equations': {
3853
+ const doc = getDoc(args?.doc_id);
3854
+ if (!doc)
3855
+ return error('Document not found');
3856
+ return success({ equations: doc.getEquations() });
3857
+ }
3858
+ case 'insert_equation': {
3859
+ const doc = getDoc(args?.doc_id);
3860
+ if (!doc)
3861
+ return error('Document not found');
3862
+ if (doc.format === 'hwp')
3863
+ return error('HWP files are read-only');
3864
+ const result = doc.insertEquation(args?.section_index, args?.after_index, args?.script);
3865
+ if (!result)
3866
+ return error('Failed to insert equation');
3867
+ return success({ message: 'Equation inserted', id: result.id });
3868
+ }
3869
+ // === Memos ===
3870
+ case 'get_memos': {
3871
+ const doc = getDoc(args?.doc_id);
3872
+ if (!doc)
3873
+ return error('Document not found');
3874
+ return success({ memos: doc.getMemos() });
3875
+ }
3876
+ case 'insert_memo': {
3877
+ const doc = getDoc(args?.doc_id);
3878
+ if (!doc)
3879
+ return error('Document not found');
3880
+ if (doc.format === 'hwp')
3881
+ return error('HWP files are read-only');
3882
+ const result = doc.insertMemo(args?.section_index, args?.paragraph_index, args?.content, args?.author);
3883
+ if (!result)
3884
+ return error('Failed to insert memo');
3885
+ return success({ message: 'Memo inserted', id: result.id });
3886
+ }
3887
+ case 'delete_memo': {
3888
+ const doc = getDoc(args?.doc_id);
3889
+ if (!doc)
3890
+ return error('Document not found');
3891
+ if (doc.format === 'hwp')
3892
+ return error('HWP files are read-only');
3893
+ if (doc.deleteMemo(args?.memo_id)) {
3894
+ return success({ message: 'Memo deleted' });
3895
+ }
3896
+ return error('Failed to delete memo');
3897
+ }
3898
+ // === Sections ===
3899
+ case 'get_sections': {
3900
+ const doc = getDoc(args?.doc_id);
3901
+ if (!doc)
3902
+ return error('Document not found');
3903
+ return success({ sections: doc.getSections() });
3904
+ }
3905
+ case 'insert_section': {
3906
+ const doc = getDoc(args?.doc_id);
3907
+ if (!doc)
3908
+ return error('Document not found');
3909
+ if (doc.format === 'hwp')
3910
+ return error('HWP files are read-only');
3911
+ const newIndex = doc.insertSection(args?.after_index);
3912
+ return success({ message: 'Section inserted', index: newIndex });
3913
+ }
3914
+ case 'delete_section': {
3915
+ const doc = getDoc(args?.doc_id);
3916
+ if (!doc)
3917
+ return error('Document not found');
3918
+ if (doc.format === 'hwp')
3919
+ return error('HWP files are read-only');
3920
+ if (doc.deleteSection(args?.section_index)) {
3921
+ return success({ message: 'Section deleted' });
3922
+ }
3923
+ return error('Failed to delete section');
3924
+ }
3925
+ case 'get_section_xml': {
3926
+ const doc = getDoc(args?.doc_id);
3927
+ if (!doc)
3928
+ return error('Document not found');
3929
+ const sectionIndex = args?.section_index ?? 0;
3930
+ const xml = await doc.getSectionXml(sectionIndex);
3931
+ if (xml === null) {
3932
+ return error(`Section ${sectionIndex} not found or document is HWP format`);
3933
+ }
3934
+ return success({ section_index: sectionIndex, xml });
3935
+ }
3936
+ case 'set_section_xml': {
3937
+ const doc = getDoc(args?.doc_id);
3938
+ if (!doc)
3939
+ return error('Document not found');
3940
+ if (doc.format === 'hwp')
3941
+ return error('HWP files are read-only');
3942
+ const sectionIndex = args?.section_index ?? 0;
3943
+ const xml = args?.xml;
3944
+ const validate = args?.validate ?? true;
3945
+ if (!xml) {
3946
+ return error('XML content is required');
3947
+ }
3948
+ const result = await doc.setSectionXml(sectionIndex, xml, validate);
3949
+ if (result.success) {
3950
+ return success({ message: `Section ${sectionIndex} XML replaced successfully` });
3951
+ }
3952
+ return error(result.error || 'Failed to set section XML');
3953
+ }
3954
+ // === Styles ===
3955
+ case 'get_styles': {
3956
+ const doc = getDoc(args?.doc_id);
3957
+ if (!doc)
3958
+ return error('Document not found');
3959
+ return success({ styles: doc.getStyles() });
3960
+ }
3961
+ case 'get_char_shapes': {
3962
+ const doc = getDoc(args?.doc_id);
3963
+ if (!doc)
3964
+ return error('Document not found');
3965
+ return success({ charShapes: doc.getCharShapes() });
3966
+ }
3967
+ case 'get_para_shapes': {
3968
+ const doc = getDoc(args?.doc_id);
3969
+ if (!doc)
3970
+ return error('Document not found');
3971
+ return success({ paraShapes: doc.getParaShapes() });
3972
+ }
3973
+ case 'apply_style': {
3974
+ const doc = getDoc(args?.doc_id);
3975
+ if (!doc)
3976
+ return error('Document not found');
3977
+ if (doc.format === 'hwp')
3978
+ return error('HWP files are read-only');
3979
+ if (doc.applyStyle(args?.section_index, args?.paragraph_index, args?.style_id)) {
3980
+ return success({ message: 'Style applied' });
3981
+ }
3982
+ return error('Failed to apply style');
3983
+ }
3984
+ // === Column Definition ===
3985
+ case 'get_column_def': {
3986
+ const doc = getDoc(args?.doc_id);
3987
+ if (!doc)
3988
+ return error('Document not found');
3989
+ return success({ columnDef: doc.getColumnDef(args?.section_index || 0) });
3990
+ }
3991
+ case 'set_column_def': {
3992
+ const doc = getDoc(args?.doc_id);
3993
+ if (!doc)
3994
+ return error('Document not found');
3995
+ if (doc.format === 'hwp')
3996
+ return error('HWP files are read-only');
3997
+ if (doc.setColumnDef(args?.section_index || 0, args?.count, args?.gap)) {
3998
+ return success({ message: 'Column definition set' });
3999
+ }
4000
+ return error('Failed to set column definition');
4001
+ }
4002
+ // === Create New Document ===
4003
+ case 'create_document': {
4004
+ const docId = generateId();
4005
+ const doc = HwpxDocument_1.HwpxDocument.createNew(docId, args?.title, args?.creator);
4006
+ openDocuments.set(docId, doc);
4007
+ return success({
4008
+ doc_id: docId,
4009
+ format: 'hwpx',
4010
+ message: 'New document created',
4011
+ });
4012
+ }
4013
+ // === XML Analysis and Repair ===
4014
+ case 'analyze_xml': {
4015
+ const doc = getDoc(args?.doc_id);
4016
+ if (!doc)
4017
+ return error('Document not found');
4018
+ const sectionIndex = args?.section_index;
4019
+ const result = await doc.analyzeXml(sectionIndex);
4020
+ return success({
4021
+ has_issues: result.hasIssues,
4022
+ summary: result.summary,
4023
+ sections: result.sections.map(s => ({
4024
+ section_index: s.sectionIndex,
4025
+ issues: s.issues,
4026
+ tag_counts: s.tagCounts,
4027
+ })),
4028
+ });
4029
+ }
4030
+ case 'repair_xml': {
4031
+ const doc = getDoc(args?.doc_id);
4032
+ if (!doc)
4033
+ return error('Document not found');
4034
+ if (doc.format === 'hwp')
4035
+ return error('HWP files are read-only');
4036
+ const sectionIndex = args?.section_index;
4037
+ if (sectionIndex === undefined)
4038
+ return error('section_index is required');
4039
+ const result = await doc.repairXml(sectionIndex, {
4040
+ removeOrphanCloseTags: args?.remove_orphan_close_tags,
4041
+ fixTableStructure: args?.fix_table_structure,
4042
+ backup: args?.backup,
4043
+ });
4044
+ return success({
4045
+ success: result.success,
4046
+ message: result.message,
4047
+ repairs_applied: result.repairsApplied,
4048
+ has_original_backup: !!result.originalXml,
4049
+ });
4050
+ }
4051
+ case 'get_raw_section_xml': {
4052
+ const doc = getDoc(args?.doc_id);
4053
+ if (!doc)
4054
+ return error('Document not found');
4055
+ const sectionIndex = args?.section_index;
4056
+ if (sectionIndex === undefined)
4057
+ return error('section_index is required');
4058
+ const xml = await doc.getRawSectionXml(sectionIndex);
4059
+ if (xml === null)
4060
+ return error(`Section ${sectionIndex} not found`);
4061
+ return success({
4062
+ section_index: sectionIndex,
4063
+ xml_length: xml.length,
4064
+ xml: xml,
4065
+ });
4066
+ }
4067
+ case 'set_raw_section_xml': {
4068
+ const doc = getDoc(args?.doc_id);
4069
+ if (!doc)
4070
+ return error('Document not found');
4071
+ if (doc.format === 'hwp')
4072
+ return error('HWP files are read-only');
4073
+ const sectionIndex = args?.section_index;
4074
+ const xml = args?.xml;
4075
+ const validate = args?.validate !== false; // default: true
4076
+ if (sectionIndex === undefined)
4077
+ return error('section_index is required');
4078
+ if (!xml)
4079
+ return error('xml is required');
4080
+ const result = await doc.setRawSectionXml(sectionIndex, xml, validate);
4081
+ if (result.success) {
4082
+ return success({
4083
+ success: true,
4084
+ message: result.message,
4085
+ });
4086
+ }
4087
+ else {
4088
+ return success({
4089
+ success: false,
4090
+ message: result.message,
4091
+ issues: result.issues,
4092
+ });
4093
+ }
4094
+ }
4095
+ // ===== Agentic Document Reading Handlers =====
4096
+ case 'chunk_document': {
4097
+ const doc = getDoc(args?.doc_id);
4098
+ if (!doc)
4099
+ return error('Document not found');
4100
+ const chunkSize = args?.chunk_size || 500;
4101
+ const overlap = args?.overlap || 100;
4102
+ const chunks = doc.chunkDocument(chunkSize, overlap);
4103
+ return success({
4104
+ total_chunks: chunks.length,
4105
+ chunk_size: chunkSize,
4106
+ overlap: overlap,
4107
+ chunks: chunks.map(c => ({
4108
+ id: c.id,
4109
+ text: c.text,
4110
+ start_offset: c.startOffset,
4111
+ end_offset: c.endOffset,
4112
+ section_index: c.sectionIndex,
4113
+ element_type: c.elementType,
4114
+ element_index: c.elementIndex,
4115
+ metadata: c.metadata,
4116
+ })),
4117
+ });
4118
+ }
4119
+ case 'search_chunks': {
4120
+ const doc = getDoc(args?.doc_id);
4121
+ if (!doc)
4122
+ return error('Document not found');
4123
+ const query = args?.query;
4124
+ if (!query)
4125
+ return error('query is required');
4126
+ const topK = args?.top_k || 5;
4127
+ const minScore = args?.min_score || 0.1;
4128
+ const results = doc.searchChunks(query, topK, minScore);
4129
+ return success({
4130
+ query,
4131
+ total_results: results.length,
4132
+ results: results.map(r => ({
4133
+ chunk_id: r.chunk.id,
4134
+ score: r.score,
4135
+ matched_terms: r.matchedTerms,
4136
+ snippet: r.snippet,
4137
+ chunk: {
4138
+ text: r.chunk.text,
4139
+ start_offset: r.chunk.startOffset,
4140
+ end_offset: r.chunk.endOffset,
4141
+ section_index: r.chunk.sectionIndex,
4142
+ element_type: r.chunk.elementType,
4143
+ metadata: r.chunk.metadata,
4144
+ },
4145
+ })),
4146
+ });
4147
+ }
4148
+ case 'get_chunk_context': {
4149
+ const doc = getDoc(args?.doc_id);
4150
+ if (!doc)
4151
+ return error('Document not found');
4152
+ const chunkId = args?.chunk_id;
4153
+ if (!chunkId)
4154
+ return error('chunk_id is required');
4155
+ const before = args?.before || 1;
4156
+ const after = args?.after || 1;
4157
+ const context = doc.getChunkContext(chunkId, before, after);
4158
+ return success({
4159
+ center_index: context.centerIndex,
4160
+ total_chunks: context.chunks.length,
4161
+ chunks: context.chunks.map(c => ({
4162
+ id: c.id,
4163
+ text: c.text,
4164
+ start_offset: c.startOffset,
4165
+ end_offset: c.endOffset,
4166
+ section_index: c.sectionIndex,
4167
+ element_type: c.elementType,
4168
+ metadata: c.metadata,
4169
+ })),
4170
+ });
4171
+ }
4172
+ case 'extract_toc': {
4173
+ const doc = getDoc(args?.doc_id);
4174
+ if (!doc)
4175
+ return error('Document not found');
4176
+ const toc = doc.extractToc();
4177
+ return success({
4178
+ total_entries: toc.length,
4179
+ toc: toc.map(t => ({
4180
+ level: t.level,
4181
+ title: t.title,
4182
+ section_index: t.sectionIndex,
4183
+ element_index: t.elementIndex,
4184
+ offset: t.offset,
4185
+ })),
4186
+ });
4187
+ }
4188
+ case 'build_position_index': {
4189
+ const doc = getDoc(args?.doc_id);
4190
+ if (!doc)
4191
+ return error('Document not found');
4192
+ const index = doc.buildPositionIndex();
4193
+ return success({
4194
+ total_entries: index.length,
4195
+ index: index.map(e => ({
4196
+ id: e.id,
4197
+ type: e.type,
4198
+ text: e.text,
4199
+ section_index: e.sectionIndex,
4200
+ element_index: e.elementIndex,
4201
+ offset: e.offset,
4202
+ level: e.level,
4203
+ table_info: e.tableInfo,
4204
+ })),
4205
+ });
4206
+ }
4207
+ case 'get_position_index': {
4208
+ const doc = getDoc(args?.doc_id);
4209
+ if (!doc)
4210
+ return error('Document not found');
4211
+ const index = doc.getPositionIndex();
4212
+ return success({
4213
+ total_entries: index.length,
4214
+ index: index.map(e => ({
4215
+ id: e.id,
4216
+ type: e.type,
4217
+ text: e.text,
4218
+ section_index: e.sectionIndex,
4219
+ element_index: e.elementIndex,
4220
+ offset: e.offset,
4221
+ level: e.level,
4222
+ table_info: e.tableInfo,
4223
+ })),
4224
+ });
4225
+ }
4226
+ case 'search_position_index': {
4227
+ const doc = getDoc(args?.doc_id);
4228
+ if (!doc)
4229
+ return error('Document not found');
4230
+ const query = args?.query;
4231
+ if (!query)
4232
+ return error('query is required');
4233
+ const type = args?.type;
4234
+ const results = doc.searchPositionIndex(query, type);
4235
+ return success({
4236
+ query,
4237
+ type_filter: type || 'all',
4238
+ total_results: results.length,
4239
+ results: results.map(e => ({
4240
+ id: e.id,
4241
+ type: e.type,
4242
+ text: e.text,
4243
+ section_index: e.sectionIndex,
4244
+ element_index: e.elementIndex,
4245
+ offset: e.offset,
4246
+ level: e.level,
4247
+ table_info: e.tableInfo,
4248
+ })),
4249
+ });
4250
+ }
4251
+ case 'get_chunk_at_offset': {
4252
+ const doc = getDoc(args?.doc_id);
4253
+ if (!doc)
4254
+ return error('Document not found');
4255
+ const offset = args?.offset;
4256
+ if (offset === undefined)
4257
+ return error('offset is required');
4258
+ const chunk = doc.getChunkAtOffset(offset);
4259
+ if (!chunk) {
4260
+ return success({ found: false, message: 'No chunk found at this offset' });
4261
+ }
4262
+ return success({
4263
+ found: true,
4264
+ chunk: {
4265
+ id: chunk.id,
4266
+ text: chunk.text,
4267
+ start_offset: chunk.startOffset,
4268
+ end_offset: chunk.endOffset,
4269
+ section_index: chunk.sectionIndex,
4270
+ element_type: chunk.elementType,
4271
+ metadata: chunk.metadata,
4272
+ },
4273
+ });
4274
+ }
4275
+ case 'invalidate_reading_cache': {
4276
+ const doc = getDoc(args?.doc_id);
4277
+ if (!doc)
4278
+ return error('Document not found');
4279
+ doc.invalidateReadingCache();
4280
+ return success({ success: true, message: 'Reading cache invalidated' });
4281
+ }
4282
+ default:
4283
+ return error(`Unknown tool: ${name}`);
4284
+ }
4285
+ }
4286
+ catch (err) {
4287
+ return error(err.message);
4288
+ }
4289
+ });
4290
+ // ============================================================
4291
+ // Helper Functions
4292
+ // ============================================================
4293
+ function getDoc(docId) {
4294
+ return openDocuments.get(docId);
4295
+ }
4296
+ function success(data) {
4297
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
4298
+ }
4299
+ function error(message) {
4300
+ return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }] };
4301
+ }
4302
+ function escapeHtml(text) {
4303
+ return text
4304
+ .replace(/&/g, '&amp;')
4305
+ .replace(/</g, '&lt;')
4306
+ .replace(/>/g, '&gt;')
4307
+ .replace(/"/g, '&quot;')
4308
+ .replace(/'/g, '&#039;');
4309
+ }
4310
+ // ============================================================
4311
+ // Main
4312
+ // ============================================================
4313
+ async function main() {
4314
+ const transport = new stdio_js_1.StdioServerTransport();
4315
+ await server.connect(transport);
4316
+ }
4317
+ main().catch(console.error);