@kimdayoun/hwpx-mcp 0.3.3 → 0.3.4

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.
@@ -0,0 +1,5 @@
1
+ import type JSZip from 'jszip';
2
+ /** First well-formedness error in `xml`, or null when it parses. */
3
+ export declare function xmlWellFormednessError(xml: string): string | null;
4
+ /** Every `.xml` / `.hpf` part of the package that does not parse, as "path: error". */
5
+ export declare function findMalformedXmlParts(zip: JSZip): Promise<string[]>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.xmlWellFormednessError = xmlWellFormednessError;
4
+ exports.findMalformedXmlParts = findMalformedXmlParts;
5
+ /**
6
+ * Well-formedness check for the XML parts of a saved HWPX package.
7
+ *
8
+ * save_document(verify_integrity) used to look for three textual symptoms
9
+ * (no `<?xml`, a dangling `<` at the end, `<` inside a tag). A section with a
10
+ * mismatched close tag passed all three and was reported as
11
+ * `integrity_verified: true`, while Hancom and every XML parser rejected it
12
+ * (reported 2026-09-24: `<hp:p>` 5730 open / 5728 close after one edit).
13
+ * A real parser is the only check that means what the flag claims.
14
+ *
15
+ * Measured on 275 Hancom-saved originals (2,046 XML parts, 168 MB): 0 false
16
+ * rejections, ~1.5 s total. The only rejected sample was a password-protected
17
+ * file, whose parts are ciphertext rather than XML.
18
+ */
19
+ const saxes_1 = require("saxes");
20
+ /** First well-formedness error in `xml`, or null when it parses. */
21
+ function xmlWellFormednessError(xml) {
22
+ // xmlns: true also rejects an undeclared prefix (<hs:sec> with only
23
+ // xmlns:hp declared). Hancom declares every prefix it uses (0 rejections
24
+ // across the 275-file corpus with this setting), while set_section_xml
25
+ // accepted a section missing xmlns:hs.
26
+ const parser = new saxes_1.SaxesParser({ xmlns: true });
27
+ let first = null;
28
+ parser.on('error', err => {
29
+ if (first === null)
30
+ first = err.message;
31
+ });
32
+ try {
33
+ parser.write(xml).close();
34
+ }
35
+ catch (err) {
36
+ first ?? (first = err instanceof Error ? err.message : String(err));
37
+ }
38
+ return first;
39
+ }
40
+ /** Every `.xml` / `.hpf` part of the package that does not parse, as "path: error". */
41
+ async function findMalformedXmlParts(zip) {
42
+ const out = [];
43
+ const names = Object.keys(zip.files)
44
+ .filter(name => !zip.files[name].dir && /\.(xml|hpf)$/i.test(name))
45
+ .sort();
46
+ for (const name of names) {
47
+ const err = xmlWellFormednessError(await zip.file(name).async('string'));
48
+ if (err)
49
+ out.push(`${name}: ${err}`);
50
+ }
51
+ return out;
52
+ }
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const HwpxDocument_1 = require("./HwpxDocument");
43
43
  const HangingIndentCalculator_1 = require("./HangingIndentCalculator");
44
+ const XmlWellFormed_1 = require("./XmlWellFormed");
44
45
  const MCP_VERSION = require('../package.json').version;
45
46
  console.error(`[HWPX MCP] Server starting - ${MCP_VERSION} - ${new Date().toISOString()}`);
46
47
  // Document storage
@@ -634,14 +635,20 @@ When NOT to use:
634
635
  description: `⭐ RECOMMENDED for finding tables. Returns ALL tables with their headers and metadata.
635
636
 
636
637
  Returns for each table:
637
- - table_index: Global index (use this for other table operations)
638
+ - section_index + table_index_in_section: pass BOTH to tools that take section_index
639
+ (update_table_cell, get_table_cell, get_table, insert_table_row, insert_table_column,
640
+ merge_cells, insert_nested_table, …)
641
+ - table_index: position across the whole document. ONLY for tools that take no
642
+ section_index (get_cell_context, batch_fill_table, insert_image_in_cell,
643
+ render_mermaid_in_cell, insert_paragraph after_table)
638
644
  - header: Text from the paragraph BEFORE the table (usually the table title)
639
645
  - size: rows × cols
640
646
  - is_empty: Whether table has content
641
647
  - first_row_preview: Preview of first row data
642
648
 
643
- Use this FIRST when working with tables, then use the table_index for:
644
- - get_table, update_table_cell, insert_image_in_cell, etc.
649
+ In a document with one section both indices are equal. With a cover section plus
650
+ a body section they differ: passing table_index to update_table_cell writes to a
651
+ DIFFERENT table (or fails) — use table_index_in_section there.
645
652
 
646
653
  Alternative tools:
647
654
  - find_table_by_header: Search by header text
@@ -2417,24 +2424,13 @@ Call get_tool_guide with: template, table, image, search, read, create`
2417
2424
  if (missingFiles.length > 0) {
2418
2425
  throw new Error(`Missing required files: ${missingFiles.join(', ')}`);
2419
2426
  }
2420
- // Verify all section XML files are valid
2421
- const sectionFiles = Object.keys(zip.files).filter(f => f.match(/^Contents\/section\d+\.xml$/));
2422
- for (const sectionFile of sectionFiles) {
2423
- const file = zip.file(sectionFile);
2424
- if (file) {
2425
- const xmlContent = await file.async('string');
2426
- if (!xmlContent || !xmlContent.includes('<?xml')) {
2427
- throw new Error(`Invalid XML in ${sectionFile}`);
2428
- }
2429
- // Check for truncated XML (incomplete tag at end)
2430
- if (xmlContent.match(/<[^>]*$/)) {
2431
- throw new Error(`Truncated XML in ${sectionFile}`);
2432
- }
2433
- // Check for broken opening tags (< followed by < without >)
2434
- if (xmlContent.match(/<[^>]*</)) {
2435
- throw new Error(`Broken tag structure in ${sectionFile}`);
2436
- }
2437
- }
2427
+ // Every XML part must actually parse. The old textual checks
2428
+ // (<?xml present, no dangling '<') passed a section with a
2429
+ // mismatched close tag, and the save reported
2430
+ // integrity_verified: true for a file Hancom cannot open.
2431
+ const malformed = await (0, XmlWellFormed_1.findMalformedXmlParts)(zip);
2432
+ if (malformed.length > 0) {
2433
+ throw new Error(`Malformed XML: ${malformed.slice(0, 3).join('; ')}`);
2438
2434
  }
2439
2435
  }
2440
2436
  catch (verifyErr) {
@@ -4352,7 +4348,11 @@ function success(data) {
4352
4348
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
4353
4349
  }
4354
4350
  function error(message) {
4355
- return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }] };
4351
+ // isError tells the MCP client the call failed. Without it, a missing
4352
+ // argument or a refused write came back as a normal result whose body merely
4353
+ // contained {"error": …}, and agents treated it as success (reported
4354
+ // 2026-09-24). The JSON body is kept for clients that read it.
4355
+ return { content: [{ type: 'text', text: JSON.stringify({ error: message }) }], isError: true };
4356
4356
  }
4357
4357
  function escapeHtml(text) {
4358
4358
  return text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kimdayoun/hwpx-mcp",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "한글 문서(HWPX)를 읽고 쓰는 MCP 서버 — 125개 도구. 문단·표·스타일·이미지·머리말/꼬리말까지 XML 수준으로 편집한다. MCP Server for Korean HWPX documents.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,6 +50,11 @@
50
50
  "prepublishOnly": "npm run build",
51
51
  "start": "node dist/index.js",
52
52
  "test": "vitest run",
53
+ "test:unit": "vitest run src tests/unit",
54
+ "test:module": "vitest run tests/module",
55
+ "test:regression": "vitest run tests/regression",
56
+ "test:e2e": "npm run build && vitest run tests/e2e",
57
+ "test:versions": "node scripts/version-matrix.mjs",
53
58
  "test:security": "npm run build && node --test test-save-security.mjs",
54
59
  "test:watch": "vitest"
55
60
  },
@@ -57,7 +62,8 @@
57
62
  "@modelcontextprotocol/sdk": "^1.0.0",
58
63
  "hwp.js": "^0.0.3",
59
64
  "jszip": "^3.10.1",
60
- "pako": "^2.1.0"
65
+ "pako": "^2.1.0",
66
+ "saxes": "^6.0.0"
61
67
  },
62
68
  "devDependencies": {
63
69
  "@types/node": "^20.0.0",