python-hwpx-automation 6.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,321 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """FastMCP runtime adapter for the canonical automation application."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ from typing import Any, cast
9
+
10
+ import mcp.types as mcp_types
11
+ from mcp.server.fastmcp import FastMCP
12
+ from hwpx_automation.office.agent import AgentContractError
13
+
14
+ from . import __version__
15
+ from .configuration import env_value
16
+ from . import quality as quality_contract
17
+ from .errors import build_error_payload, mcp_code_for_error
18
+ from .hwpx_ops import HwpxOperationError
19
+ from .network_policy import NetworkPolicyError
20
+ from .tool_contract import (
21
+ register_fastmcp_tools,
22
+ )
23
+ from .workspace import (
24
+ WorkspaceConfigurationError,
25
+ WorkspacePathError,
26
+ )
27
+ from .fastmcp_adapter import configure_runtime
28
+ from .runtime_services import RUNTIME_SERVICES
29
+ from .tool_bindings import TOOL_BINDINGS
30
+
31
+
32
+ def _error_data(
33
+ payload: dict[str, Any],
34
+ *,
35
+ tool_name: str | None = None,
36
+ extra_data: dict | None = None,
37
+ ) -> mcp_types.ErrorData:
38
+ error_code = str(payload["code"])
39
+ data: dict[str, object] = {
40
+ "errorCode": error_code,
41
+ "error": payload,
42
+ }
43
+ if tool_name is not None:
44
+ data["tool"] = tool_name
45
+ if extra_data:
46
+ data.update(extra_data)
47
+ return mcp_types.ErrorData(
48
+ code=mcp_code_for_error(error_code),
49
+ message=f"{error_code}: {payload['message']}",
50
+ data=data,
51
+ )
52
+
53
+
54
+ def _first_text_content(content: object) -> str | None:
55
+ if not isinstance(content, list):
56
+ return None
57
+ for item in content:
58
+ text = getattr(item, "text", None)
59
+ if isinstance(text, str):
60
+ stripped = text.strip()
61
+ if stripped:
62
+ return stripped
63
+ if isinstance(item, dict):
64
+ value = item.get("text")
65
+ if isinstance(value, str):
66
+ stripped = value.strip()
67
+ if stripped:
68
+ return stripped
69
+ return None
70
+
71
+
72
+ def _exception_chain(exc: BaseException) -> list[BaseException]:
73
+ chain: list[BaseException] = []
74
+ current: BaseException | None = exc
75
+ while current is not None and current not in chain:
76
+ chain.append(current)
77
+ current = current.__cause__ or current.__context__
78
+ return chain
79
+
80
+
81
+ def _classified_error_payload(exc: BaseException | None) -> dict[str, Any]:
82
+ if exc is not None:
83
+ for item in _exception_chain(exc):
84
+ if isinstance(item, HwpxOperationError):
85
+ return item.to_payload()
86
+ if isinstance(item, AgentContractError):
87
+ return build_error_payload(
88
+ code=item.code,
89
+ message="문서 에이전트 계약을 안전하게 해석할 수 없습니다.",
90
+ )
91
+ if isinstance(item, WorkspacePathError):
92
+ return build_error_payload(
93
+ code=item.code,
94
+ message="요청한 경로가 허용된 HWPX 작업공간 경계를 벗어났습니다.",
95
+ details=item.safe_details(),
96
+ )
97
+ if isinstance(item, WorkspaceConfigurationError):
98
+ return build_error_payload(
99
+ code=item.code,
100
+ message="HWPX 작업공간 루트 구성이 유효하지 않습니다.",
101
+ )
102
+ if isinstance(item, NetworkPolicyError):
103
+ return build_error_payload(
104
+ code=item.code,
105
+ message="기본 네트워크 정책이 요청한 대상을 차단했습니다.",
106
+ details=item.safe_details(),
107
+ )
108
+ if isinstance(item, FileNotFoundError):
109
+ return build_error_payload(
110
+ code="DOCUMENT_NOT_FOUND",
111
+ message="요청한 문서를 허용된 작업공간에서 찾을 수 없습니다.",
112
+ )
113
+ if isinstance(item, PermissionError):
114
+ return build_error_payload(
115
+ code="PERMISSION_DENIED",
116
+ message="요청한 문서에 접근할 권한이 없습니다.",
117
+ )
118
+ if isinstance(item, ValueError):
119
+ return build_error_payload(
120
+ code="INVALID_ARGUMENT",
121
+ message="도구 인자가 스키마 또는 값 제약을 만족하지 않습니다.",
122
+ )
123
+ return build_error_payload(
124
+ code="TOOL_EXECUTION_FAILED",
125
+ message="도구 실행이 안전하게 완료되지 않았습니다.",
126
+ )
127
+
128
+
129
+ def _gate_or_plain_error(
130
+ tool_name: str,
131
+ *,
132
+ exc: BaseException | None = None,
133
+ ) -> mcp_types.ErrorData:
134
+ """Rebuild a structured gate/skew error from the stash, else a plain error."""
135
+
136
+ gate = quality_contract.take_last_gate_error()
137
+ if isinstance(gate, quality_contract.CapabilitySkewError):
138
+ return _error_data(
139
+ build_error_payload(
140
+ code=gate.code,
141
+ message="설치된 core/automation/plugin 기능 계약이 일치하지 않아 쓰기를 차단했습니다.",
142
+ details={"capability": gate.state},
143
+ ),
144
+ tool_name=tool_name,
145
+ extra_data={"errorCode": gate.code, "capability": gate.state},
146
+ )
147
+ if isinstance(gate, quality_contract.QualityGateError):
148
+ return _error_data(
149
+ build_error_payload(
150
+ code=gate.code,
151
+ message="문서 품질 게이트가 출력을 거부했습니다.",
152
+ details={"visualComplete": gate.block},
153
+ retryable=True,
154
+ ),
155
+ tool_name=tool_name,
156
+ extra_data={
157
+ "errorCode": gate.code,
158
+ "visualComplete": gate.block,
159
+ "suggestedRetry": gate.block.get("suggestedRetry"),
160
+ },
161
+ )
162
+ return _error_data(_classified_error_payload(exc), tool_name=tool_name)
163
+
164
+
165
+ def _failure_code_from_payload(payload: object) -> str | None:
166
+ if not isinstance(payload, dict):
167
+ return None
168
+ failed = payload.get("ok") is False or payload.get("success") is False
169
+ failed = failed or payload.get("isError") is True
170
+ if not failed:
171
+ return None
172
+ candidates: list[object] = [payload.get("errorCode"), payload.get("code")]
173
+ nested_error = payload.get("error")
174
+ if isinstance(nested_error, dict):
175
+ candidates.extend([nested_error.get("errorCode"), nested_error.get("code")])
176
+ nested_errors = payload.get("errors")
177
+ if (
178
+ isinstance(nested_errors, list)
179
+ and nested_errors
180
+ and isinstance(nested_errors[0], dict)
181
+ ):
182
+ candidates.extend(
183
+ [nested_errors[0].get("errorCode"), nested_errors[0].get("code")]
184
+ )
185
+ for candidate in candidates:
186
+ if isinstance(candidate, str) and re.fullmatch(
187
+ r"[A-Z][A-Z0-9_]{2,63}", candidate
188
+ ):
189
+ return candidate
190
+ return "TOOL_EXECUTION_FAILED"
191
+
192
+
193
+ def _result_failure_error(
194
+ tool_name: str, payload: object
195
+ ) -> mcp_types.ErrorData | None:
196
+ error_code = _failure_code_from_payload(payload)
197
+ if error_code is None:
198
+ return None
199
+ gate_error = _gate_or_plain_error(tool_name)
200
+ if cast(dict[str, Any], gate_error.data).get("errorCode") != "TOOL_EXECUTION_FAILED":
201
+ return gate_error
202
+ return _error_data(
203
+ build_error_payload(
204
+ code=error_code,
205
+ message="도구가 요청을 안전하게 완료하지 못했습니다.",
206
+ ),
207
+ tool_name=tool_name,
208
+ )
209
+
210
+
211
+ async def _strict_call_tool_handler(req: mcp_types.CallToolRequest):
212
+ tool_name = req.params.name
213
+ arguments = req.params.arguments or {}
214
+ quality_contract.clear_last_gate_error()
215
+ try:
216
+ result = await RUNTIME_SERVICES.require_mcp().call_tool(tool_name, arguments)
217
+ except Exception as exc:
218
+ # FastMCP wraps a tool's exception in ToolError, so the structured gate/
219
+ # skew error never matches a specific `except` here — recover it from the
220
+ # stash the exception left on construction (plan §2 Phase F).
221
+ return _gate_or_plain_error(tool_name, exc=exc)
222
+
223
+ if isinstance(result, mcp_types.CreateTaskResult):
224
+ return mcp_types.ServerResult(result)
225
+
226
+ if isinstance(result, mcp_types.CallToolResult):
227
+ if bool(result.isError):
228
+ return _gate_or_plain_error(tool_name)
229
+ embedded_failure = _result_failure_error(tool_name, result.structuredContent)
230
+ if embedded_failure is not None:
231
+ return embedded_failure
232
+ return mcp_types.ServerResult(result)
233
+
234
+ if isinstance(result, tuple) and len(result) == 2:
235
+ unstructured_content = list(result[0])
236
+ structured_content = result[1]
237
+ elif isinstance(result, dict):
238
+ structured_content = result
239
+ unstructured_content = [
240
+ mcp_types.TextContent(
241
+ type="text", text=json.dumps(result, ensure_ascii=False, indent=2)
242
+ )
243
+ ]
244
+ elif isinstance(result, str):
245
+ structured_content = None
246
+ unstructured_content = [mcp_types.TextContent(type="text", text=result)]
247
+ elif hasattr(result, "__iter__"):
248
+ structured_content = None
249
+ unstructured_content = list(result)
250
+ else:
251
+ return _error_data(
252
+ build_error_payload(
253
+ code="TOOL_EXECUTION_FAILED",
254
+ message="도구가 지원되지 않는 결과 형식을 반환했습니다.",
255
+ details={"resultType": type(result).__name__},
256
+ ),
257
+ tool_name=tool_name,
258
+ )
259
+
260
+ embedded_failure = _result_failure_error(tool_name, structured_content)
261
+ if embedded_failure is not None:
262
+ return embedded_failure
263
+
264
+ return mcp_types.ServerResult(
265
+ mcp_types.CallToolResult(
266
+ content=cast(list[mcp_types.ContentBlock], unstructured_content),
267
+ structuredContent=cast(dict[str, Any] | None, structured_content),
268
+ isError=False,
269
+ )
270
+ )
271
+
272
+
273
+ def _advanced_enabled() -> bool:
274
+ return env_value("ADVANCED", "0") == "1"
275
+
276
+
277
+ def _compose_runtime(*, advanced: bool, replace: bool) -> tuple[FastMCP, Any]:
278
+ composed = FastMCP("python-hwpx-automation")
279
+ if replace:
280
+ RUNTIME_SERVICES.reconfigure_runtime(
281
+ mcp=composed,
282
+ active_advanced=advanced,
283
+ tool_bindings=TOOL_BINDINGS,
284
+ )
285
+ else:
286
+ RUNTIME_SERVICES.configure_runtime(
287
+ mcp=composed,
288
+ active_advanced=advanced,
289
+ tool_bindings=TOOL_BINDINGS,
290
+ )
291
+ configure_runtime(composed, __version__, _strict_call_tool_handler)
292
+ registry = register_fastmcp_tools(composed, TOOL_BINDINGS, advanced=advanced)
293
+ RUNTIME_SERVICES.install_registry(registry)
294
+ return composed, registry
295
+
296
+
297
+ def refresh_runtime_for_environment() -> None:
298
+ """Recompose only when the released import-time profile flag changed."""
299
+
300
+ global ACTIVE_ADVANCED, TOOL_REGISTRY, mcp
301
+ requested = _advanced_enabled()
302
+ if requested == ACTIVE_ADVANCED:
303
+ return
304
+ mcp, TOOL_REGISTRY = _compose_runtime(advanced=requested, replace=True)
305
+ ACTIVE_ADVANCED = requested
306
+
307
+
308
+ ACTIVE_ADVANCED = _advanced_enabled()
309
+ mcp, TOOL_REGISTRY = _compose_runtime(
310
+ advanced=ACTIVE_ADVANCED,
311
+ replace=RUNTIME_SERVICES.mcp is not None,
312
+ )
313
+
314
+
315
+ __all__ = [
316
+ "ACTIVE_ADVANCED",
317
+ "TOOL_REGISTRY",
318
+ "mcp",
319
+ "refresh_runtime_for_environment",
320
+ "_strict_call_tool_handler",
321
+ ]
@@ -0,0 +1,100 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Typed process services shared by plain MCP handler callables.
3
+
4
+ The container owns mutable process configuration, while the canonical handler
5
+ callables and ToolSpec binding map remain immutable. Handler modules depend on
6
+ this neutral container and never import the server facade or runtime composer.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable, Mapping
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, TYPE_CHECKING
14
+
15
+ from .hwpx_ops import HwpxOps
16
+
17
+ if TYPE_CHECKING:
18
+ from mcp.server.fastmcp import FastMCP
19
+
20
+ from .tool_contract import RegisteredToolRegistry
21
+
22
+
23
+ ToolBindings = Mapping[str, Callable[..., Any]]
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class RuntimeServices:
28
+ """One typed service graph for FastMCP and durable workflow dispatch."""
29
+
30
+ ops: HwpxOps = field(default_factory=lambda: HwpxOps(auto_backup=False))
31
+ idempotency_cache: dict[str, dict[str, Any]] = field(default_factory=dict)
32
+ max_idempotency_cache_entries: int = 512
33
+ active_advanced: bool = False
34
+ mcp: FastMCP | None = None
35
+ tool_registry: RegisteredToolRegistry | None = None
36
+ tool_bindings: ToolBindings | None = None
37
+
38
+ def configure_runtime(
39
+ self,
40
+ *,
41
+ mcp: FastMCP,
42
+ active_advanced: bool,
43
+ tool_bindings: ToolBindings,
44
+ ) -> None:
45
+ """Bind transport and the one immutable callable map exactly once."""
46
+
47
+ if self.mcp is not None and self.mcp is not mcp:
48
+ raise RuntimeError("RuntimeServices FastMCP runtime is already bound")
49
+ if self.tool_bindings is not None and self.tool_bindings is not tool_bindings:
50
+ raise RuntimeError("RuntimeServices tool bindings are already bound")
51
+ self.mcp = mcp
52
+ self.active_advanced = active_advanced
53
+ self.tool_bindings = tool_bindings
54
+
55
+ def reconfigure_runtime(
56
+ self,
57
+ *,
58
+ mcp: FastMCP,
59
+ active_advanced: bool,
60
+ tool_bindings: ToolBindings,
61
+ ) -> None:
62
+ """Replace only the import-time transport/profile composition.
63
+
64
+ This preserves the released ``reload(server)`` environment-profile seam
65
+ without replacing the singleton service graph referenced by handlers.
66
+ """
67
+
68
+ if self.tool_bindings is not None and self.tool_bindings is not tool_bindings:
69
+ raise RuntimeError("RuntimeServices tool bindings are already bound")
70
+ self.mcp = mcp
71
+ self.active_advanced = active_advanced
72
+ self.tool_bindings = tool_bindings
73
+ self.tool_registry = None
74
+
75
+ def install_registry(self, registry: RegisteredToolRegistry) -> None:
76
+ if self.tool_registry is not None and self.tool_registry is not registry:
77
+ if self.tool_registry.binding_hash() != registry.binding_hash():
78
+ raise RuntimeError("RuntimeServices canonical registry is already bound")
79
+ self.tool_registry = registry
80
+
81
+ def replace_ops(self, ops: HwpxOps) -> None:
82
+ """Replace the stable compatibility facade for an authorized workspace."""
83
+
84
+ self.ops = ops
85
+
86
+ def require_mcp(self) -> FastMCP:
87
+ if self.mcp is None:
88
+ raise RuntimeError("FastMCP runtime is not configured")
89
+ return self.mcp
90
+
91
+ def require_tool_bindings(self) -> ToolBindings:
92
+ if self.tool_bindings is None:
93
+ raise RuntimeError("canonical tool bindings are not configured")
94
+ return self.tool_bindings
95
+
96
+
97
+ RUNTIME_SERVICES = RuntimeServices()
98
+
99
+
100
+ __all__ = ["RUNTIME_SERVICES", "RuntimeServices", "ToolBindings"]
@@ -0,0 +1,259 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Stable FastMCP import and CLI facade over cohesive handler owners."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+
10
+ from .configuration import env_int, env_value
11
+ from .handlers import _shared, authoring, quality_render, read_export, specialized
12
+ from . import quality as _quality_contract
13
+ from .hwpx_ops import HwpxOps
14
+ from . import runtime as _runtime
15
+ from .runtime_services import RUNTIME_SERVICES
16
+ from .storage import LocalDocumentStorage
17
+ from .tool_bindings import TOOL_BINDINGS
18
+ from .workspace import (
19
+ WORKSPACE_ROOTS_ENV,
20
+ WorkspaceConfigurationError,
21
+ )
22
+
23
+
24
+ _runtime.refresh_runtime_for_environment()
25
+ ACTIVE_ADVANCED = _runtime.ACTIVE_ADVANCED
26
+ TOOL_REGISTRY = _runtime.TOOL_REGISTRY
27
+ _strict_call_tool_handler = _runtime._strict_call_tool_handler
28
+ mcp = _runtime.mcp
29
+ quality_contract = _quality_contract
30
+ save_doc = _shared.save_doc
31
+ build_hwpx_verification_report = _shared.build_hwpx_verification_report
32
+ _save_generated_document = authoring._save_generated_document
33
+ resolve_oracle = specialized.resolve_oracle
34
+ measure_question_splits = specialized.measure_question_splits
35
+ render_glyph_boxes = specialized.render_glyph_boxes
36
+ extract_image_boxes = specialized.extract_image_boxes
37
+ RemoteRenderClientV2 = _shared.RemoteRenderClientV2
38
+
39
+
40
+ # Explicit stable re-exports. These are the exact objects in TOOL_BINDINGS.
41
+ get_document_node = TOOL_BINDINGS["get_document_node"]
42
+ query_document_nodes = TOOL_BINDINGS["query_document_nodes"]
43
+ apply_document_commands = TOOL_BINDINGS["apply_document_commands"]
44
+ dump_document_blueprint = TOOL_BINDINGS["dump_document_blueprint"]
45
+ replay_document_blueprint = TOOL_BINDINGS["replay_document_blueprint"]
46
+ get_document_text = TOOL_BINDINGS["get_document_text"]
47
+ get_document_info = TOOL_BINDINGS["get_document_info"]
48
+ get_document_outline = TOOL_BINDINGS["get_document_outline"]
49
+ get_document_map = TOOL_BINDINGS["get_document_map"]
50
+ get_paragraph_text = TOOL_BINDINGS["get_paragraph_text"]
51
+ get_paragraphs_text = TOOL_BINDINGS["get_paragraphs_text"]
52
+ get_location_text = TOOL_BINDINGS["get_location_text"]
53
+ get_table_text = TOOL_BINDINGS["get_table_text"]
54
+ get_table_map = TOOL_BINDINGS["get_table_map"]
55
+ find_text = TOOL_BINDINGS["find_text"]
56
+ list_available_documents = TOOL_BINDINGS["list_available_documents"]
57
+ hwpx_to_markdown = TOOL_BINDINGS["hwpx_to_markdown"]
58
+ document_to_markdown = TOOL_BINDINGS["document_to_markdown"]
59
+ hwpx_to_html = TOOL_BINDINGS["hwpx_to_html"]
60
+ hwpx_extract_json = TOOL_BINDINGS["hwpx_extract_json"]
61
+ document_extract_json = TOOL_BINDINGS["document_extract_json"]
62
+ package_parts = TOOL_BINDINGS["package_parts"]
63
+ package_get_text = TOOL_BINDINGS["package_get_text"]
64
+ package_get_xml = TOOL_BINDINGS["package_get_xml"]
65
+ object_find_by_attr = TOOL_BINDINGS["object_find_by_attr"]
66
+ object_find_by_tag = TOOL_BINDINGS["object_find_by_tag"]
67
+ create_document = TOOL_BINDINGS["create_document"]
68
+ create_document_from_plan = TOOL_BINDINGS["create_document_from_plan"]
69
+ copy_document = TOOL_BINDINGS["copy_document"]
70
+ create_government_report_document = TOOL_BINDINGS["create_government_report_document"]
71
+ create_proposal_document = TOOL_BINDINGS["create_proposal_document"]
72
+ create_comparison_table_document = TOOL_BINDINGS["create_comparison_table_document"]
73
+ get_document_plan_schema = TOOL_BINDINGS["get_document_plan_schema"]
74
+ validate_document_plan = TOOL_BINDINGS["validate_document_plan"]
75
+ analyze_document_plan = TOOL_BINDINGS["analyze_document_plan"]
76
+ markdown_to_document_plan = TOOL_BINDINGS["markdown_to_document_plan"]
77
+ parse_government_report_text = TOOL_BINDINGS["parse_government_report_text"]
78
+ compute_report_value = TOOL_BINDINGS["compute_report_value"]
79
+ register_template = TOOL_BINDINGS["register_template"]
80
+ list_templates = TOOL_BINDINGS["list_templates"]
81
+ describe_template = TOOL_BINDINGS["describe_template"]
82
+ add_heading = TOOL_BINDINGS["add_heading"]
83
+ add_paragraph = TOOL_BINDINGS["add_paragraph"]
84
+ insert_paragraph = TOOL_BINDINGS["insert_paragraph"]
85
+ delete_paragraph = TOOL_BINDINGS["delete_paragraph"]
86
+ add_page_break = TOOL_BINDINGS["add_page_break"]
87
+ apply_edits = TOOL_BINDINGS["apply_edits"]
88
+ undo_last_edit = TOOL_BINDINGS["undo_last_edit"]
89
+ replace_by_anchor = TOOL_BINDINGS["replace_by_anchor"]
90
+ replace_in_paragraph = TOOL_BINDINGS["replace_in_paragraph"]
91
+ search_and_replace = TOOL_BINDINGS["search_and_replace"]
92
+ batch_replace = TOOL_BINDINGS["batch_replace"]
93
+ byte_preserving_patch = TOOL_BINDINGS["byte_preserving_patch"]
94
+ insert_picture = TOOL_BINDINGS["insert_picture"]
95
+ replace_picture = TOOL_BINDINGS["replace_picture"]
96
+ add_table = TOOL_BINDINGS["add_table"]
97
+ set_table_cell_text = TOOL_BINDINGS["set_table_cell_text"]
98
+ merge_table_cells = TOOL_BINDINGS["merge_table_cells"]
99
+ split_table_cell = TOOL_BINDINGS["split_table_cell"]
100
+ format_table = TOOL_BINDINGS["format_table"]
101
+ table_compute = TOOL_BINDINGS["table_compute"]
102
+ list_styles = TOOL_BINDINGS["list_styles"]
103
+ create_custom_style = TOOL_BINDINGS["create_custom_style"]
104
+ set_paragraph_format = TOOL_BINDINGS["set_paragraph_format"]
105
+ set_list_format = TOOL_BINDINGS["set_list_format"]
106
+ format_text = TOOL_BINDINGS["format_text"]
107
+ extract_style_profile = TOOL_BINDINGS["extract_style_profile"]
108
+ apply_style_profile_to_plan = TOOL_BINDINGS["apply_style_profile_to_plan"]
109
+ compare_style_profiles = TOOL_BINDINGS["compare_style_profiles"]
110
+ set_page_setup = TOOL_BINDINGS["set_page_setup"]
111
+ set_header_footer = TOOL_BINDINGS["set_header_footer"]
112
+ set_page_number = TOOL_BINDINGS["set_page_number"]
113
+ add_toc = TOOL_BINDINGS["add_toc"]
114
+ add_cross_reference = TOOL_BINDINGS["add_cross_reference"]
115
+ verify_toc = TOOL_BINDINGS["verify_toc"]
116
+ add_memo = TOOL_BINDINGS["add_memo"]
117
+ add_memo_by_anchor = TOOL_BINDINGS["add_memo_by_anchor"]
118
+ remove_memo = TOOL_BINDINGS["remove_memo"]
119
+ scan_form_guidance = TOOL_BINDINGS["scan_form_guidance"]
120
+ apply_table_ops = TOOL_BINDINGS["apply_table_ops"]
121
+ apply_body_ops = TOOL_BINDINGS["apply_body_ops"]
122
+ inspect_fill_residue = TOOL_BINDINGS["inspect_fill_residue"]
123
+ verify_form_fill = TOOL_BINDINGS["verify_form_fill"]
124
+ list_form_fields = TOOL_BINDINGS["list_form_fields"]
125
+ fill_form_field = TOOL_BINDINGS["fill_form_field"]
126
+ find_cell_by_label = TOOL_BINDINGS["find_cell_by_label"]
127
+ fill_by_path = TOOL_BINDINGS["fill_by_path"]
128
+ analyze_form_fill = TOOL_BINDINGS["analyze_form_fill"]
129
+ apply_form_fill = TOOL_BINDINGS["apply_form_fill"]
130
+ analyze_template_formfit = TOOL_BINDINGS["analyze_template_formfit"]
131
+ apply_template_formfit = TOOL_BINDINGS["apply_template_formfit"]
132
+ apply_evalplan_fill = TOOL_BINDINGS["apply_evalplan_fill"]
133
+ score_form_fill = TOOL_BINDINGS["score_form_fill"]
134
+ add_tracked_edit = TOOL_BINDINGS["add_tracked_edit"]
135
+ scan_personal_info = TOOL_BINDINGS["scan_personal_info"]
136
+ compose_exam = TOOL_BINDINGS["compose_exam"]
137
+ verify_question_splits = TOOL_BINDINGS["verify_question_splits"]
138
+ place_seal = TOOL_BINDINGS["place_seal"]
139
+ check_seal_compliance = TOOL_BINDINGS["check_seal_compliance"]
140
+ mail_merge = TOOL_BINDINGS["mail_merge"]
141
+ inspect_mail_merge_placeholders = TOOL_BINDINGS["inspect_mail_merge_placeholders"]
142
+ build_image_grid = TOOL_BINDINGS["build_image_grid"]
143
+ build_meeting_nameplates = TOOL_BINDINGS["build_meeting_nameplates"]
144
+ build_organization_chart = TOOL_BINDINGS["build_organization_chart"]
145
+ render_submit = TOOL_BINDINGS["render_submit"]
146
+ render_status = TOOL_BINDINGS["render_status"]
147
+ render_cancel = TOOL_BINDINGS["render_cancel"]
148
+ render_health = TOOL_BINDINGS["render_health"]
149
+ render_preview = TOOL_BINDINGS["render_preview"]
150
+ repair_hwpx = TOOL_BINDINGS["repair_hwpx"]
151
+ mcp_server_health = TOOL_BINDINGS["mcp_server_health"]
152
+ describe_capabilities = TOOL_BINDINGS["describe_capabilities"]
153
+ doc_diff = TOOL_BINDINGS["doc_diff"]
154
+ validate_structure = TOOL_BINDINGS["validate_structure"]
155
+ lint_text_conventions = TOOL_BINDINGS["lint_text_conventions"]
156
+ inspect_document_quality = TOOL_BINDINGS["inspect_document_quality"]
157
+ inspect_document_authoring_quality = TOOL_BINDINGS["inspect_document_authoring_quality"]
158
+ inspect_operating_plan_quality = TOOL_BINDINGS["inspect_operating_plan_quality"]
159
+ inspect_official_document_style = TOOL_BINDINGS["inspect_official_document_style"]
160
+ inspect_reference_consistency = TOOL_BINDINGS["inspect_reference_consistency"]
161
+ start_workflow = TOOL_BINDINGS["start_workflow"]
162
+ get_workflow = TOOL_BINDINGS["get_workflow"]
163
+ get_workflow_result = TOOL_BINDINGS["get_workflow_result"]
164
+ continue_workflow = TOOL_BINDINGS["continue_workflow"]
165
+ approve_workflow_decision = TOOL_BINDINGS["approve_workflow_decision"]
166
+ cancel_workflow = TOOL_BINDINGS["cancel_workflow"]
167
+ resume_workflow = TOOL_BINDINGS["resume_workflow"]
168
+
169
+ _SERVER_TOOL_BINDINGS = TOOL_BINDINGS
170
+ _TOOL_REGISTRY = TOOL_REGISTRY
171
+ _ACTIVE_ADVANCED = ACTIVE_ADVANCED
172
+ _OPS = RUNTIME_SERVICES.ops
173
+ _fastmcp_tool_names = quality_render._fastmcp_tool_names
174
+ _render_client = quality_render._render_client
175
+ _table_count = read_export._table_count
176
+
177
+
178
+ def _replace_ops(ops: HwpxOps) -> None:
179
+ global _OPS
180
+ RUNTIME_SERVICES.replace_ops(ops)
181
+ _OPS = ops
182
+
183
+
184
+ def main(argv: list[str] | None = None) -> None:
185
+ parser = argparse.ArgumentParser(prog="hwpx-automation-mcp")
186
+ parser.add_argument(
187
+ "--transport",
188
+ choices=("stdio", "streamable-http", "http"),
189
+ default=env_value("TRANSPORT", "stdio"),
190
+ help="MCP transport to use",
191
+ )
192
+ parser.add_argument(
193
+ "--host",
194
+ default=env_value("HOST", "127.0.0.1"),
195
+ help="Host interface for streamable HTTP transport",
196
+ )
197
+ parser.add_argument(
198
+ "--port",
199
+ type=int,
200
+ default=max(1, env_int("PORT", 8000)),
201
+ help="TCP port for streamable HTTP transport",
202
+ )
203
+ parser.add_argument(
204
+ "--workspace-root",
205
+ action="append",
206
+ default=None,
207
+ help=(
208
+ "Authorized local workspace root. Repeat for multiple roots; relative paths use the first. "
209
+ f"Equivalent to {WORKSPACE_ROOTS_ENV}."
210
+ ),
211
+ )
212
+ args = parser.parse_args(argv)
213
+
214
+ if args.workspace_root:
215
+ os.environ[WORKSPACE_ROOTS_ENV] = json.dumps(args.workspace_root)
216
+ try:
217
+ storage = LocalDocumentStorage(auto_backup=False)
218
+ except WorkspaceConfigurationError as exc:
219
+ # Explicit HWPX_AUTOMATION_WORKSPACE_ROOTS (or its supported 6.x
220
+ # HWPX_MCP_WORKSPACE_ROOTS fallback) / --workspace-root configuration is
221
+ # invalid: fail fast so the operator fixes the value. An unconfigured
222
+ # degenerate cwd instead defers inside LocalDocumentStorage so the server
223
+ # still boots and every document tool call reports WORKSPACE_ROOT_INVALID.
224
+ parser.error(
225
+ f"invalid HWPX workspace configuration: {exc}. "
226
+ f"Set {WORKSPACE_ROOTS_ENV} to existing project directories or launch from the project workspace."
227
+ )
228
+ _replace_ops(HwpxOps(storage=storage))
229
+
230
+ selected_transport = args.transport
231
+ if selected_transport == "http":
232
+ selected_transport = "streamable-http"
233
+
234
+ if selected_transport == "stdio":
235
+ mcp.run(transport="stdio")
236
+ return
237
+
238
+ # TODO: add pluggable auth middleware/headers for production HTTP deployments.
239
+ try: # HTTP transport is an optional [http] extra
240
+ import uvicorn
241
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
242
+ raise RuntimeError(
243
+ "HTTP transport requires the 'uvicorn' package. Install it with: "
244
+ "pip install 'python-hwpx-automation[http]' "
245
+ "(or 'hwpx-mcp-server[http]', or pip install uvicorn). "
246
+ "The default stdio transport needs no extra."
247
+ ) from exc
248
+
249
+ app = mcp.streamable_http_app()
250
+ uvicorn.run(
251
+ app,
252
+ host=args.host,
253
+ port=args.port,
254
+ log_level=os.environ.get("LOG_LEVEL", "info").lower(),
255
+ )
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()