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,672 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Sole fail-closed FastMCP compatibility seam for the server runtime.
3
+
4
+ Ordinary registration stays on FastMCP's public API. The SDK does not expose
5
+ public hooks for the initialize version, the raw ``CallToolRequest`` handler,
6
+ or callable-aware live registry snapshots, so those exact operations remain
7
+ isolated here. Every other module and test consumes the project-owned APIs
8
+ below and never reaches into FastMCP implementation details.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import copy
14
+ import functools
15
+ import inspect
16
+ from dataclasses import dataclass
17
+ from importlib.metadata import PackageNotFoundError, version as distribution_version
18
+ from types import MappingProxyType
19
+ from typing import Annotated, Any, Awaitable, Callable, Mapping, cast, get_args, get_origin
20
+
21
+ import mcp.types as mcp_types
22
+ from mcp.server.fastmcp import FastMCP
23
+ from pydantic import TypeAdapter
24
+
25
+ from .execution_lock import PUBLIC_MUTATION_LOCK
26
+
27
+
28
+ # The package resolver pin and this audited allowlist MUST admit the same
29
+ # set: anything pip can install must also start. Admitting a new patch is an
30
+ # explicit re-audit (docs/mcp-sdk-reaudit.md) that updates both together.
31
+ SUPPORTED_MCP_RANGE = "==1.28.1"
32
+ AUDITED_MCP_PATCHES = ("1.28.1",)
33
+
34
+
35
+ class FastMcpAdapterError(RuntimeError):
36
+ """FastMCP cannot provide the registry guarantees required by the server."""
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class FastMcpToolSnapshot:
41
+ name: str
42
+ callable: Callable[..., Any]
43
+ description: str
44
+ input_schema: dict[str, Any]
45
+ output_schema: dict[str, Any]
46
+ meta: dict[str, Any]
47
+
48
+
49
+ StrictCallHandler = Callable[[mcp_types.CallToolRequest], Awaitable[Any]]
50
+
51
+
52
+ def _installed_mcp_version() -> str:
53
+ try:
54
+ return distribution_version("mcp")
55
+ except PackageNotFoundError as exc: # pragma: no cover - required dependency
56
+ raise FastMcpAdapterError("the MCP SDK distribution is unavailable") from exc
57
+
58
+
59
+ def _require_audited_mcp_patch() -> str:
60
+ installed = _installed_mcp_version()
61
+ if installed not in AUDITED_MCP_PATCHES:
62
+ audited = ", ".join(AUDITED_MCP_PATCHES)
63
+ raise FastMcpAdapterError(
64
+ f"unsupported MCP SDK {installed!r}; the package pins mcp"
65
+ f"{SUPPORTED_MCP_RANGE} and only audited patches are admitted "
66
+ f"({audited}) — see docs/mcp-sdk-reaudit.md"
67
+ )
68
+ return installed
69
+
70
+
71
+ def _low_level_runtime(mcp: Any) -> tuple[Any, dict[Any, Any]]:
72
+ """Return the pinned low-level runtime shape or fail before mutation."""
73
+
74
+ _require_audited_mcp_patch()
75
+ runtime = getattr(mcp, "_mcp_server", None)
76
+ if runtime is None or not hasattr(runtime, "version"):
77
+ raise FastMcpAdapterError(
78
+ "FastMCP low-level runtime/version hook is unavailable for the audited SDK"
79
+ )
80
+ request_handlers = getattr(runtime, "request_handlers", None)
81
+ if not isinstance(request_handlers, dict):
82
+ raise FastMcpAdapterError(
83
+ "FastMCP request handler registry has an incompatible shape"
84
+ )
85
+ existing = request_handlers.get(mcp_types.CallToolRequest)
86
+ if not callable(existing):
87
+ raise FastMcpAdapterError(
88
+ "FastMCP CallToolRequest handler is unavailable for the audited SDK"
89
+ )
90
+ return runtime, request_handlers
91
+
92
+
93
+ def configure_runtime(
94
+ mcp: Any,
95
+ package_version: str,
96
+ strict_call_handler: StrictCallHandler,
97
+ ) -> None:
98
+ """Set initialize version and the strict raw call handler atomically.
99
+
100
+ FastMCP 1.28.1 exposes neither operation through its public constructor or
101
+ decorators. Shape and audited-patch checks happen before mutation; any
102
+ failed write restores the prior runtime state and raises the adapter-owned
103
+ error type.
104
+ """
105
+
106
+ if not isinstance(package_version, str) or not package_version.strip():
107
+ raise FastMcpAdapterError("package_version must be a non-empty string")
108
+ if not callable(strict_call_handler):
109
+ raise FastMcpAdapterError("strict_call_handler must be callable")
110
+
111
+ runtime, request_handlers = _low_level_runtime(mcp)
112
+ previous_version = runtime.version
113
+ previous_handler = request_handlers[mcp_types.CallToolRequest]
114
+ try:
115
+ runtime.version = package_version
116
+ request_handlers[mcp_types.CallToolRequest] = strict_call_handler
117
+ if runtime.version != package_version:
118
+ raise FastMcpAdapterError(
119
+ "FastMCP did not retain the configured server version"
120
+ )
121
+ if request_handlers.get(mcp_types.CallToolRequest) is not strict_call_handler:
122
+ raise FastMcpAdapterError("FastMCP did not retain the strict call handler")
123
+ except Exception as exc:
124
+ try:
125
+ runtime.version = previous_version
126
+ request_handlers[mcp_types.CallToolRequest] = previous_handler
127
+ except Exception:
128
+ pass
129
+ if isinstance(exc, FastMcpAdapterError):
130
+ raise
131
+ raise FastMcpAdapterError(
132
+ f"cannot configure the FastMCP runtime: {exc}"
133
+ ) from exc
134
+
135
+
136
+ def runtime_server_version(mcp: Any) -> str:
137
+ """Return the configured initialize version without exposing SDK internals."""
138
+
139
+ runtime, _ = _low_level_runtime(mcp)
140
+ configured = runtime.version
141
+ if not isinstance(configured, str) or not configured:
142
+ raise FastMcpAdapterError("FastMCP runtime has no configured server version")
143
+ return configured
144
+
145
+
146
+ def normalize_schema(schema: Mapping[str, Any] | None, *, input_schema: bool) -> dict[str, Any]:
147
+ """Return a deterministic JSON-compatible schema.
148
+
149
+ Object inputs are closed at every generated object boundary unless their
150
+ schema explicitly declares an open mapping. Array order is preserved so
151
+ catalog-owned schemas remain byte-for-byte attributable to their source.
152
+ MCP restricts both tool input and output schemas to an object at the root;
153
+ Pydantic root-model unions need that discriminator restored explicitly.
154
+ """
155
+
156
+ def visit(value: Any) -> Any:
157
+ if isinstance(value, Mapping):
158
+ out = {str(key): visit(nested) for key, nested in sorted(value.items(), key=lambda item: str(item[0]))}
159
+ # A ``properties`` member can itself be a user field name inside a
160
+ # JSON Schema property map. Only explicit object-schema nodes are
161
+ # eligible for generated closure defaults.
162
+ if out.get("type") == "object":
163
+ composed = any(
164
+ keyword in out for keyword in ("$ref", "allOf", "anyOf", "oneOf")
165
+ )
166
+ if "properties" in out or not composed:
167
+ out.setdefault("properties", {})
168
+ if "additionalProperties" not in out:
169
+ out["additionalProperties"] = (
170
+ False if out["properties"] else True
171
+ )
172
+ return out
173
+ if isinstance(value, (list, tuple)):
174
+ return [visit(item) for item in value]
175
+ if value is None or isinstance(value, (str, int, float, bool)):
176
+ return value
177
+ raise FastMcpAdapterError(f"schema contains non-JSON value: {type(value).__name__}")
178
+
179
+ normalized = visit(dict(schema or {}))
180
+ if not isinstance(normalized, dict): # pragma: no cover - defensive
181
+ raise FastMcpAdapterError("tool schema must be a JSON object")
182
+ if input_schema:
183
+ normalized.setdefault("type", "object")
184
+ normalized.setdefault("properties", {})
185
+ # A tool call is never an open mapping, including zero-argument tools.
186
+ # Nested mapping values keep their explicitly generated openness.
187
+ normalized["additionalProperties"] = False
188
+ normalized.setdefault("required", [])
189
+ else:
190
+ root_type = normalized.get("type")
191
+ if root_type not in (None, "object"):
192
+ raise FastMcpAdapterError(
193
+ f"MCP output schema root must be object, got {root_type!r}"
194
+ )
195
+ normalized.setdefault("type", "object")
196
+ if len(normalized) == 1:
197
+ normalized.update({"properties": {}, "additionalProperties": True})
198
+ return normalized
199
+
200
+
201
+ def _resolved_annotations(func: Callable[..., Any]) -> dict[str, Any]:
202
+ """Resolve annotations without Python 3.10's implicit Optional rewrite."""
203
+
204
+ candidates: list[Callable[..., Any]] = []
205
+ seen: set[int] = set()
206
+ current = func
207
+ while callable(current) and id(current) not in seen:
208
+ seen.add(id(current))
209
+ candidates.append(current)
210
+ nested = getattr(current, "__hwpx_original_callable__", current)
211
+ if nested is current:
212
+ break
213
+ current = nested
214
+ for candidate in candidates:
215
+ try:
216
+ return dict(inspect.get_annotations(candidate, eval_str=True))
217
+ except (NameError, TypeError, ValueError):
218
+ continue
219
+ for candidate in reversed(candidates):
220
+ raw = getattr(candidate, "__annotations__", None)
221
+ if isinstance(raw, Mapping):
222
+ return dict(raw)
223
+ return {}
224
+
225
+
226
+ def _resolved_return_annotation(func: Callable[..., Any]) -> Any:
227
+ return _resolved_annotations(func).get(
228
+ "return", inspect.signature(func).return_annotation
229
+ )
230
+
231
+
232
+ def _annotation_schema_extra(annotation: Any) -> dict[str, Any]:
233
+ """Collect mapping-valued Field.json_schema_extra without reordering it."""
234
+
235
+ extras: dict[str, Any] = {}
236
+ while get_origin(annotation) is Annotated:
237
+ nested, *metadata = get_args(annotation)
238
+ for item in metadata:
239
+ extra = getattr(item, "json_schema_extra", None)
240
+ if isinstance(extra, Mapping):
241
+ extras.update(copy.deepcopy(dict(extra)))
242
+ annotation = nested
243
+ return extras
244
+
245
+
246
+ def _restore_parameter_schema_extras(
247
+ schema: Mapping[str, Any] | None,
248
+ func: Callable[..., Any],
249
+ ) -> dict[str, Any]:
250
+ """Restore catalog-owned schema fragments that Pydantic may reorder.
251
+
252
+ Pydantic faithfully carries ``json_schema_extra`` into the generated
253
+ schema, but it normalizes nested ``required`` arrays. The shared agent
254
+ catalog is the canonical byte-level source, so overlay the original extras
255
+ after generation before storing the FastMCP registry receipt.
256
+ """
257
+
258
+ restored = copy.deepcopy(dict(schema or {}))
259
+ properties = restored.get("properties")
260
+ if not isinstance(properties, dict):
261
+ return restored
262
+ hints = _resolved_annotations(func)
263
+ for name, annotation in hints.items():
264
+ if name == "return" or name not in properties:
265
+ continue
266
+ extra = _annotation_schema_extra(annotation)
267
+ if extra and isinstance(properties[name], dict):
268
+ properties[name].update(extra)
269
+ top_level_extra = getattr(func, "__hwpx_input_schema_extra__", None)
270
+ if isinstance(top_level_extra, Mapping):
271
+ restored.update(copy.deepcopy(dict(top_level_extra)))
272
+ return restored
273
+
274
+
275
+ def _normalize_implicit_none_parameters(
276
+ schema: Mapping[str, Any] | None,
277
+ func: Callable[..., Any],
278
+ ) -> dict[str, Any]:
279
+ """Remove Python-version-specific implicit nullability.
280
+
281
+ A number of legacy callables use ``value: T = None`` without declaring
282
+ ``T | None``. Pydantic emits different schemas for that invalid typing
283
+ pattern on Python 3.10 and newer interpreters. The public contract follows
284
+ the explicit annotation: omitted arguments may still receive their default,
285
+ but an explicitly supplied JSON ``null`` is accepted only when the
286
+ annotation itself includes ``None``. Catalog-owned schema extras are
287
+ restored after this normalization and therefore remain authoritative.
288
+ """
289
+
290
+ normalized = copy.deepcopy(dict(schema or {}))
291
+ properties = normalized.get("properties")
292
+ if not isinstance(properties, dict):
293
+ return normalized
294
+ source = original_callable(func)
295
+ try:
296
+ # Unlike get_type_hints(), inspect.get_annotations() does not rewrite
297
+ # ``T = None`` into Optional[T] on Python 3.10. That distinction is the
298
+ # exact source declaration this normalization must preserve.
299
+ hints = inspect.get_annotations(source, eval_str=True)
300
+ except (NameError, TypeError, ValueError):
301
+ hints = dict(getattr(source, "__annotations__", {}))
302
+ try:
303
+ signature = inspect.signature(source)
304
+ except (TypeError, ValueError): # pragma: no cover - binding checks first
305
+ return normalized
306
+
307
+ for name, parameter in signature.parameters.items():
308
+ if parameter.default is not None or name not in properties:
309
+ continue
310
+ annotation = hints.get(name, parameter.annotation)
311
+ while get_origin(annotation) is Annotated:
312
+ annotation = get_args(annotation)[0]
313
+ annotation_args = get_args(annotation)
314
+ if isinstance(annotation, str) and (
315
+ "None" in annotation or "Optional" in annotation
316
+ ):
317
+ continue
318
+ if (
319
+ annotation is None
320
+ or annotation is type(None)
321
+ or None in annotation_args
322
+ or type(None) in annotation_args
323
+ ):
324
+ continue
325
+ property_schema = properties[name]
326
+ if not isinstance(property_schema, dict):
327
+ continue
328
+ options = property_schema.get("anyOf")
329
+ if not isinstance(options, list):
330
+ continue
331
+ retained = [
332
+ option
333
+ for option in options
334
+ if not (isinstance(option, Mapping) and option.get("type") == "null")
335
+ ]
336
+ if len(retained) == len(options) or not retained:
337
+ continue
338
+ siblings = {
339
+ key: value for key, value in property_schema.items() if key != "anyOf"
340
+ }
341
+ if len(retained) == 1 and isinstance(retained[0], Mapping):
342
+ properties[name] = {**copy.deepcopy(dict(retained[0])), **siblings}
343
+ else:
344
+ properties[name] = {**siblings, "anyOf": retained}
345
+ return normalize_schema(normalized, input_schema=True)
346
+
347
+
348
+ def _registration_callable(func: Callable[..., Any]) -> tuple[Callable[..., Any], bool]:
349
+ """Normalize bare ``dict`` returns for structured output without altering API functions."""
350
+
351
+ # Annotated metadata is contract-critical: the core agent catalog binds its
352
+ # closed command/blueprint schemas through Field(json_schema_extra=...).
353
+ resolved_hints = _resolved_annotations(func)
354
+ return_annotation = resolved_hints.get("return", inspect.signature(func).return_annotation)
355
+ call_result, call_result_model = _call_tool_result_output_model(return_annotation)
356
+ if call_result:
357
+ if call_result_model is None:
358
+ raise FastMcpAdapterError(
359
+ "CallToolResult returns require Annotated[CallToolResult, OutputModel] "
360
+ "so outputSchema validates structuredContent"
361
+ )
362
+ return func, True
363
+ if return_annotation is not dict:
364
+ return func, return_annotation is not inspect.Signature.empty and return_annotation is not Any
365
+
366
+ @functools.wraps(func)
367
+ def structured_wrapper(*args: Any, **kwargs: Any) -> dict[str, Any]:
368
+ return func(*args, **kwargs)
369
+
370
+ original_signature = inspect.signature(func)
371
+ structured_wrapper.__signature__ = original_signature.replace( # type: ignore[attr-defined]
372
+ parameters=[
373
+ parameter.replace(annotation=resolved_hints.get(parameter.name, parameter.annotation))
374
+ for parameter in original_signature.parameters.values()
375
+ ],
376
+ return_annotation=dict[str, Any],
377
+ )
378
+ structured_wrapper.__annotations__ = {
379
+ **resolved_hints,
380
+ "return": dict[str, Any],
381
+ }
382
+ setattr(structured_wrapper, "__hwpx_original_callable__", func)
383
+ return structured_wrapper, True
384
+
385
+
386
+ def _call_tool_result_output_model(annotation: Any) -> tuple[bool, Any | None]:
387
+ """Return whether *annotation* is CallToolResult and its owned output model."""
388
+
389
+ metadata: tuple[Any, ...] = ()
390
+ base = annotation
391
+ if get_origin(annotation) is Annotated:
392
+ base, *nested_metadata = get_args(annotation)
393
+ metadata = tuple(nested_metadata)
394
+ try:
395
+ is_call_result = isinstance(base, type) and issubclass(
396
+ base, mcp_types.CallToolResult
397
+ )
398
+ except TypeError: # pragma: no cover - defensive for exotic annotations
399
+ is_call_result = False
400
+ return is_call_result, metadata[0] if is_call_result and metadata else None
401
+
402
+
403
+ def _serialize_mutation_callable(
404
+ func: Callable[..., Any],
405
+ *,
406
+ mutates: bool,
407
+ ) -> Callable[..., Any]:
408
+ """Serialize catalog-declared writers without changing their tool schema."""
409
+
410
+ if not mutates:
411
+ return func
412
+
413
+ @functools.wraps(func)
414
+ def serialized(*args: Any, **kwargs: Any) -> Any:
415
+ with PUBLIC_MUTATION_LOCK:
416
+ return func(*args, **kwargs)
417
+
418
+ # The public server module uses postponed annotations. A wrapper defined
419
+ # in this adapter has a different globals namespace, so copying annotation
420
+ # strings verbatim makes FastMCP try to resolve e.g. ``mcp_types`` here and
421
+ # can incorrectly treat CallToolResult as structured output. Bind the
422
+ # already-resolved objects onto both the signature and annotations.
423
+ resolved_hints = _resolved_annotations(func)
424
+ original_signature = inspect.signature(func)
425
+ serialized.__signature__ = original_signature.replace( # type: ignore[attr-defined]
426
+ parameters=[
427
+ parameter.replace(
428
+ annotation=resolved_hints.get(parameter.name, parameter.annotation)
429
+ )
430
+ for parameter in original_signature.parameters.values()
431
+ ],
432
+ return_annotation=resolved_hints.get(
433
+ "return", original_signature.return_annotation
434
+ ),
435
+ )
436
+ serialized.__annotations__ = resolved_hints
437
+ setattr(serialized, "__hwpx_original_callable__", original_callable(func))
438
+ return serialized
439
+
440
+
441
+ def original_callable(func: Callable[..., Any]) -> Callable[..., Any]:
442
+ """Return the public server function behind an adapter-generated wrapper."""
443
+
444
+ seen: set[int] = set()
445
+ current = func
446
+ while callable(current) and id(current) not in seen:
447
+ seen.add(id(current))
448
+ nested = getattr(current, "__hwpx_original_callable__", current)
449
+ if nested is current:
450
+ return current
451
+ current = nested
452
+ return current
453
+
454
+
455
+ def _manager_tools(mcp: Any) -> dict[str, Any]:
456
+ _require_audited_mcp_patch()
457
+ manager = getattr(mcp, "_tool_manager", None)
458
+ tools = getattr(manager, "_tools", None)
459
+ if manager is None or not isinstance(tools, dict):
460
+ raise FastMcpAdapterError(
461
+ "FastMCP tool manager is unavailable; supported registry introspection is required"
462
+ )
463
+ return tools
464
+
465
+
466
+ def _register(
467
+ mcp: Any,
468
+ *,
469
+ name: str,
470
+ func: Callable[..., Any],
471
+ description: str,
472
+ meta: Mapping[str, Any],
473
+ ) -> FastMcpToolSnapshot:
474
+ _require_audited_mcp_patch()
475
+ adapted, structured_output = _registration_callable(func)
476
+ adapted = _serialize_mutation_callable(
477
+ adapted,
478
+ mutates=bool(meta.get("hwpxMutates")),
479
+ )
480
+ tool_method = getattr(mcp, "tool", None)
481
+ if not callable(tool_method):
482
+ raise FastMcpAdapterError("FastMCP.tool is unavailable")
483
+ try:
484
+ parameters = inspect.signature(tool_method).parameters
485
+ except (TypeError, ValueError) as exc:
486
+ raise FastMcpAdapterError(f"cannot inspect FastMCP.tool: {exc}") from exc
487
+ required_parameters = {"name", "description", "meta", "structured_output"}
488
+ missing_parameters = sorted(required_parameters - set(parameters))
489
+ if missing_parameters:
490
+ raise FastMcpAdapterError(
491
+ "FastMCP.tool lacks audited registration parameters: "
492
+ + ", ".join(missing_parameters)
493
+ )
494
+ live_tools = _manager_tools(mcp)
495
+ if name in live_tools:
496
+ raise FastMcpAdapterError(f"FastMCP already contains tool {name!r}")
497
+ kwargs: dict[str, Any] = {
498
+ "name": name,
499
+ "description": description,
500
+ "meta": dict(meta),
501
+ "structured_output": structured_output,
502
+ }
503
+ cast(Callable[..., Any], tool_method)(**kwargs)(adapted)
504
+
505
+ tool = _manager_tools(mcp).get(name)
506
+ if tool is None:
507
+ raise FastMcpAdapterError(f"FastMCP did not retain registered tool {name!r}")
508
+ registered_callable = getattr(tool, "fn", None)
509
+ if not callable(registered_callable):
510
+ raise FastMcpAdapterError(f"registered tool {name!r} has no callable")
511
+ raw_parameters = getattr(tool, "parameters", None)
512
+ if not isinstance(raw_parameters, Mapping):
513
+ raise FastMcpAdapterError(
514
+ f"registered tool {name!r} has no mapping input schema"
515
+ )
516
+ registered_description = getattr(tool, "description", None)
517
+ if not isinstance(registered_description, str):
518
+ raise FastMcpAdapterError(f"registered tool {name!r} has no description")
519
+ registered_meta = getattr(tool, "meta", None)
520
+ if not isinstance(registered_meta, Mapping):
521
+ raise FastMcpAdapterError(f"registered tool {name!r} has no metadata mapping")
522
+
523
+ input_json = normalize_schema(raw_parameters, input_schema=True)
524
+ input_json = _normalize_implicit_none_parameters(input_json, func)
525
+ # Overlay catalog fragments after normalization so their original array
526
+ # order and intentional open-object semantics remain exact.
527
+ input_json = _restore_parameter_schema_extras(input_json, func)
528
+ raw_output = getattr(tool, "output_schema", None)
529
+ if not isinstance(raw_output, Mapping):
530
+ raise FastMcpAdapterError(
531
+ f"FastMCP did not publish a mapping output schema for {name!r}"
532
+ )
533
+ output_json = normalize_schema(raw_output, input_schema=False)
534
+
535
+ call_result, call_result_model = _call_tool_result_output_model(
536
+ _resolved_return_annotation(func)
537
+ )
538
+ if call_result:
539
+ assert call_result_model is not None # guarded by _registration_callable
540
+ try:
541
+ expected_output = normalize_schema(
542
+ TypeAdapter(call_result_model).json_schema(),
543
+ input_schema=False,
544
+ )
545
+ except Exception as exc:
546
+ raise FastMcpAdapterError(
547
+ f"cannot derive structuredContent schema for {name!r}: {exc}"
548
+ ) from exc
549
+ if output_json != expected_output:
550
+ raise FastMcpAdapterError(
551
+ f"FastMCP did not honor the Annotated CallToolResult output model "
552
+ f"for {name!r}; mcp{SUPPORTED_MCP_RANGE} is required"
553
+ )
554
+
555
+ # Make the live MCP schema byte-for-byte comparable with the canonical
556
+ # registry. CallToolResult tools advertise their source-owned structured
557
+ # content model, never the outer MCP response envelope.
558
+ try:
559
+ tool.parameters = input_json
560
+ if structured_output:
561
+ tool.output_schema = output_json
562
+ tool.description = description
563
+ tool.meta = dict(meta)
564
+ except Exception as exc:
565
+ raise FastMcpAdapterError(
566
+ f"cannot normalize registered tool {name!r}: {exc}"
567
+ ) from exc
568
+ if tool.parameters != input_json or tool.description != description:
569
+ raise FastMcpAdapterError(
570
+ f"FastMCP did not retain normalized schema/description for {name!r}"
571
+ )
572
+ if structured_output and tool.output_schema != output_json:
573
+ raise FastMcpAdapterError(
574
+ f"FastMCP did not retain the normalized output schema for {name!r}"
575
+ )
576
+ if tool.meta != dict(meta):
577
+ raise FastMcpAdapterError(f"FastMCP did not retain metadata for {name!r}")
578
+ return FastMcpToolSnapshot(
579
+ name=name,
580
+ callable=original_callable(registered_callable),
581
+ description=tool.description,
582
+ input_schema=input_json,
583
+ output_schema=output_json,
584
+ meta=dict(tool.meta),
585
+ )
586
+
587
+
588
+ def describe_callables(
589
+ entries: list[tuple[str, Callable[..., Any], str, Mapping[str, Any]]],
590
+ ) -> dict[str, FastMcpToolSnapshot]:
591
+ """Build schemas for every public callable, including inactive advanced tools."""
592
+
593
+ schema_server = FastMCP("hwpx-tool-contract-schema")
594
+ snapshots: dict[str, FastMcpToolSnapshot] = {}
595
+ for name, func, description, meta in entries:
596
+ snapshots[name] = _register(
597
+ schema_server,
598
+ name=name,
599
+ func=func,
600
+ description=description,
601
+ meta=meta,
602
+ )
603
+ return snapshots
604
+
605
+
606
+ def register_canonical_tool(
607
+ mcp: Any,
608
+ *,
609
+ name: str,
610
+ func: Callable[..., Any],
611
+ description: str,
612
+ meta: Mapping[str, Any],
613
+ ) -> FastMcpToolSnapshot:
614
+ """Register one canonical callable through the public SDK hook."""
615
+
616
+ return _register(mcp, name=name, func=func, description=description, meta=meta)
617
+
618
+
619
+ def snapshot_runtime_tools(mcp: Any) -> Mapping[str, FastMcpToolSnapshot]:
620
+ """Return an insertion-ordered, read-only snapshot of the live registry."""
621
+
622
+ snapshots: dict[str, FastMcpToolSnapshot] = {}
623
+ for name, tool in _manager_tools(mcp).items():
624
+ if not isinstance(name, str) or not name:
625
+ raise FastMcpAdapterError("FastMCP retained a non-string tool name")
626
+ registered = getattr(tool, "fn", None)
627
+ if not callable(registered):
628
+ raise FastMcpAdapterError(f"registered tool {name!r} has no callable")
629
+ func = original_callable(registered)
630
+ description = getattr(tool, "description", None)
631
+ if not isinstance(description, str):
632
+ raise FastMcpAdapterError(f"registered tool {name!r} has no description")
633
+ meta = getattr(tool, "meta", None)
634
+ if not isinstance(meta, Mapping):
635
+ raise FastMcpAdapterError(f"registered tool {name!r} has no metadata mapping")
636
+ raw_parameters = getattr(tool, "parameters", None)
637
+ if not isinstance(raw_parameters, Mapping):
638
+ raise FastMcpAdapterError(
639
+ f"registered tool {name!r} has no mapping input schema"
640
+ )
641
+ raw_output = getattr(tool, "output_schema", None)
642
+ if not isinstance(raw_output, Mapping):
643
+ raise FastMcpAdapterError(
644
+ f"registered tool {name!r} has no mapping output schema"
645
+ )
646
+ input_schema = normalize_schema(raw_parameters, input_schema=True)
647
+ input_schema = _normalize_implicit_none_parameters(input_schema, func)
648
+ input_schema = _restore_parameter_schema_extras(input_schema, func)
649
+ snapshots[name] = FastMcpToolSnapshot(
650
+ name=name,
651
+ callable=func,
652
+ description=description,
653
+ input_schema=input_schema,
654
+ output_schema=normalize_schema(raw_output, input_schema=False),
655
+ meta=dict(meta),
656
+ )
657
+ return MappingProxyType(snapshots)
658
+
659
+
660
+ __all__ = [
661
+ "AUDITED_MCP_PATCHES",
662
+ "FastMcpAdapterError",
663
+ "FastMcpToolSnapshot",
664
+ "SUPPORTED_MCP_RANGE",
665
+ "configure_runtime",
666
+ "describe_callables",
667
+ "normalize_schema",
668
+ "original_callable",
669
+ "register_canonical_tool",
670
+ "runtime_server_version",
671
+ "snapshot_runtime_tools",
672
+ ]