@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.
- package/CHANGELOG.md +53 -0
- package/README.md +13 -3
- package/dist/HwpxDocument.d.ts +123 -0
- package/dist/HwpxDocument.js +828 -202
- package/dist/XmlWellFormed.d.ts +5 -0
- package/dist/XmlWellFormed.js +52 -0
- package/dist/index.js +22 -22
- package/package.json +8 -2
|
@@ -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
|
-
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
-
//
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|