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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- 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()
|