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,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
|
+
]
|