diff --git a/README.md b/README.md index eef5ad11..b31fb8be 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-19) — MCP Structured Output](#whats-new-2026-06-19--mcp-structured-output) - [What's new (2026-06-19) — Tweened Drag](#whats-new-2026-06-19--tweened-drag) - [What's new (2026-06-19) — Process-Doc (SOP) Generator](#whats-new-2026-06-19--process-doc-sop-generator) - [What's new (2026-06-19) — Heal Analytics & Secret Scan](#whats-new-2026-06-19--heal-analytics--secret-scan) @@ -82,6 +83,12 @@ --- +## What's new (2026-06-19) — MCP Structured Output + +MCP 2025-06-18 structured tool output. Full reference: [`docs/source/Eng/doc/new_features/v30_features_doc.rst`](docs/source/Eng/doc/new_features/v30_features_doc.rst). + +- **`MCPTool(output_schema=...)`** — a tool may declare an `outputSchema`; its dict result is returned as `structuredContent` in the `tools/call` response so clients/LLMs consume a typed, schema-validated object instead of re-parsing text. `to_descriptor()` advertises it in `tools/list`; non-dict results and schema-less tools are unchanged. `ac_validate_rows` is the first built-in to adopt it. + ## What's new (2026-06-19) — Tweened Drag Deterministic eased drags. Full reference: [`docs/source/Eng/doc/new_features/v29_features_doc.rst`](docs/source/Eng/doc/new_features/v29_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 3532ed2d..a7d80ea5 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-19) — MCP 结构化输出](#本次更新-2026-06-19--mcp-结构化输出) - [本次更新 (2026-06-19) — 缓动拖拽](#本次更新-2026-06-19--缓动拖拽) - [本次更新 (2026-06-19) — 流程文档(SOP)生成器](#本次更新-2026-06-19--流程文档sop生成器) - [本次更新 (2026-06-19) — 修复分析与机密扫描](#本次更新-2026-06-19--修复分析与机密扫描) @@ -81,6 +82,12 @@ --- +## 本次更新 (2026-06-19) — MCP 结构化输出 + +MCP 2025-06-18 结构化工具输出。完整参考:[`docs/source/Zh/doc/new_features/v30_features_doc.rst`](../docs/source/Zh/doc/new_features/v30_features_doc.rst)。 + +- **`MCPTool(output_schema=...)`** — 工具可声明 `outputSchema`;其 dict 结果会在 `tools/call` 响应以 `structuredContent` 返回,让客户端/LLM 消费类型化、经 schema 验证的对象而非重新解析文本。`to_descriptor()` 会在 `tools/list` 公告;非 dict 结果与未声明 schema 的工具行为不变。`ac_validate_rows` 为首个采用。 + ## 本次更新 (2026-06-19) — 缓动拖拽 确定性的缓动拖拽。完整参考:[`docs/source/Zh/doc/new_features/v29_features_doc.rst`](../docs/source/Zh/doc/new_features/v29_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 10163151..1aeba0a0 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-19) — MCP 結構化輸出](#本次更新-2026-06-19--mcp-結構化輸出) - [本次更新 (2026-06-19) — 緩動拖曳](#本次更新-2026-06-19--緩動拖曳) - [本次更新 (2026-06-19) — 流程文件(SOP)產生器](#本次更新-2026-06-19--流程文件sop產生器) - [本次更新 (2026-06-19) — 修復分析與機密掃描](#本次更新-2026-06-19--修復分析與機密掃描) @@ -81,6 +82,12 @@ --- +## 本次更新 (2026-06-19) — MCP 結構化輸出 + +MCP 2025-06-18 結構化工具輸出。完整參考:[`docs/source/Zh/doc/new_features/v30_features_doc.rst`](../docs/source/Zh/doc/new_features/v30_features_doc.rst)。 + +- **`MCPTool(output_schema=...)`** — 工具可宣告 `outputSchema`;其 dict 結果會在 `tools/call` 回應以 `structuredContent` 回傳,讓用戶端/LLM 消費型別化、經 schema 驗證的物件而非重新解析文字。`to_descriptor()` 會在 `tools/list` 公告;非 dict 結果與未宣告 schema 的工具行為不變。`ac_validate_rows` 為首個採用。 + ## 本次更新 (2026-06-19) — 緩動拖曳 決定性的緩動拖曳。完整參考:[`docs/source/Zh/doc/new_features/v29_features_doc.rst`](../docs/source/Zh/doc/new_features/v29_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v30_features_doc.rst b/docs/source/Eng/doc/new_features/v30_features_doc.rst new file mode 100644 index 00000000..09f1cd51 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v30_features_doc.rst @@ -0,0 +1,40 @@ +================================================== +New Features (2026-06-19) — MCP Structured Tool Output +================================================== + +The MCP server now supports the 2025-06-18 **structured tool output** spec +feature: a tool may declare an ``outputSchema``, and its dict result is +returned as ``structuredContent`` in the ``tools/call`` response (alongside +the usual text ``content``). MCP clients and LLMs can then consume a typed, +schema-validated object instead of re-parsing text — saving tokens and +errors. Pure standard library; an MCP-framework enhancement (no new +``AC_*`` command). + +.. contents:: + :local: + :depth: 2 + + +Declaring an output schema +========================= + +:: + + from je_auto_control.utils.mcp_server.tools import MCPTool + + MCPTool( + name="ac_validate_rows", description="...", + input_schema=..., handler=validate_rows, + output_schema={"type": "object", "properties": { + "ok": {"type": "boolean"}, "errors": {"type": "array"}}}, + ) + +``to_descriptor()`` then includes ``outputSchema`` in ``tools/list``, and a +``tools/call`` whose handler returns a ``dict`` includes +``structuredContent`` in the result. Tools without an ``outputSchema`` are +unchanged; non-dict results never produce ``structuredContent``. + +The first built-in tool to adopt this is ``ac_validate_rows`` (data-quality +validation), whose ``{ok, total, valid_count, invalid_count, errors, ...}`` +result is now schema-typed; more tools can opt in by adding an +``output_schema``. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 93fc81a6..dd786831 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -52,6 +52,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v27_features_doc doc/new_features/v28_features_doc doc/new_features/v29_features_doc + doc/new_features/v30_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v30_features_doc.rst b/docs/source/Zh/doc/new_features/v30_features_doc.rst new file mode 100644 index 00000000..b0df1297 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v30_features_doc.rst @@ -0,0 +1,37 @@ +================================================== +新功能 (2026-06-19) — MCP 結構化工具輸出 +================================================== + +MCP 伺服器現在支援 2025-06-18 的**結構化工具輸出**規範:工具可宣告 +``outputSchema``,其 dict 結果會在 ``tools/call`` 回應中以 +``structuredContent`` 回傳(與一般的文字 ``content`` 並列)。MCP 用戶端 +與 LLM 即可消費經 schema 驗證的型別化物件,而非重新解析文字——省 token、 +少出錯。純標準庫;屬 MCP 框架增強(不新增 ``AC_*`` 指令)。 + +.. contents:: + :local: + :depth: 2 + + +宣告輸出 schema +=============== + +:: + + from je_auto_control.utils.mcp_server.tools import MCPTool + + MCPTool( + name="ac_validate_rows", description="...", + input_schema=..., handler=validate_rows, + output_schema={"type": "object", "properties": { + "ok": {"type": "boolean"}, "errors": {"type": "array"}}}, + ) + +``to_descriptor()`` 會在 ``tools/list`` 中包含 ``outputSchema``;當 +``tools/call`` 的 handler 回傳 ``dict`` 時,結果會包含 ``structuredContent``。 +未宣告 ``outputSchema`` 的工具行為不變;非 dict 結果不會產生 +``structuredContent``。 + +首個採用的內建工具是 ``ac_validate_rows``(資料品質驗證),其 +``{ok, total, valid_count, invalid_count, errors, ...}`` 結果現在已型別化; +其他工具可透過加上 ``output_schema`` 逐步採用。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index c0b46b6b..99d51998 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -52,6 +52,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v27_features_doc doc/new_features/v28_features_doc doc/new_features/v29_features_doc + doc/new_features/v30_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/utils/mcp_server/server.py b/je_auto_control/utils/mcp_server/server.py index 16c1ace7..3dbc8610 100644 --- a/je_auto_control/utils/mcp_server/server.py +++ b/je_auto_control/utils/mcp_server/server.py @@ -580,10 +580,15 @@ def _handle_tools_call(self, msg_id: Any, tool=name, arguments=arguments, status="ok", duration_seconds=time.monotonic() - started_at, ) - return { + response: Dict[str, Any] = { "content": _to_content_blocks(result), "isError": False, } + # 2025-06-18 spec: tools with an outputSchema return their dict result + # as structuredContent for typed, token-cheap client consumption. + if tool.output_schema is not None and isinstance(result, dict): + response["structuredContent"] = result + return response def request_elicitation(self, message: str, requested_schema: Optional[Dict[str, Any]] = None, diff --git a/je_auto_control/utils/mcp_server/tools/_base.py b/je_auto_control/utils/mcp_server/tools/_base.py index 9159cd6e..e4b83682 100644 --- a/je_auto_control/utils/mcp_server/tools/_base.py +++ b/je_auto_control/utils/mcp_server/tools/_base.py @@ -86,15 +86,19 @@ class MCPTool: input_schema: Dict[str, Any] handler: Callable[..., Any] annotations: MCPToolAnnotations = MCPToolAnnotations() + output_schema: Optional[Dict[str, Any]] = None def to_descriptor(self) -> Dict[str, Any]: """Return the dict shape MCP clients expect from ``tools/list``.""" - return { + descriptor: Dict[str, Any] = { "name": self.name, "description": self.description, "inputSchema": self.input_schema, "annotations": self.annotations.to_dict(), } + if self.output_schema is not None: + descriptor["outputSchema"] = self.output_schema + return descriptor def invoke(self, arguments: Dict[str, Any], ctx: Any = None) -> Any: """Call the underlying handler with keyword arguments. diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 8a92d078..3fcaffbc 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -2193,6 +2193,15 @@ def data_quality_tools() -> List[MCPTool]: "rows": {"type": "array", "items": {"type": "object"}}, "schema": {"type": "object"}, }, required=["rows", "schema"]), + output_schema=schema({ + "ok": {"type": "boolean"}, + "total": {"type": "integer"}, + "valid_count": {"type": "integer"}, + "invalid_count": {"type": "integer"}, + "valid": {"type": "array"}, + "invalid": {"type": "array"}, + "errors": {"type": "array", "items": {"type": "object"}}, + }, required=["ok", "errors"]), handler=h.validate_rows, annotations=READ_ONLY, ), diff --git a/test/unit_test/headless/test_mcp_structured_output.py b/test/unit_test/headless/test_mcp_structured_output.py new file mode 100644 index 00000000..3ff3ea6e --- /dev/null +++ b/test/unit_test/headless/test_mcp_structured_output.py @@ -0,0 +1,48 @@ +"""Headless tests for MCP structured tool output (2025-06-18 spec): +optional outputSchema on a tool + structuredContent in the tools/call +result. Pure stdlib; no Qt imports.""" +from je_auto_control.utils.mcp_server.server import MCPServer +from je_auto_control.utils.mcp_server.tools import ( + MCPTool, build_default_tool_registry) + +_EMPTY_INPUT = {"type": "object", "properties": {}} +_OUT = {"type": "object", "properties": {"value": {"type": "integer"}}} + + +def test_output_schema_in_descriptor(): + typed = MCPTool(name="t", description="d", input_schema=_EMPTY_INPUT, + handler=lambda: {"value": 1}, output_schema=_OUT) + plain = MCPTool(name="p", description="d", input_schema=_EMPTY_INPUT, + handler=lambda: {"value": 1}) + assert typed.to_descriptor()["outputSchema"] == _OUT + assert "outputSchema" not in plain.to_descriptor() + + +def test_structured_content_only_with_output_schema(): + typed = MCPTool(name="typed", description="d", input_schema=_EMPTY_INPUT, + handler=lambda: {"value": 42}, output_schema=_OUT) + plain = MCPTool(name="plain", description="d", input_schema=_EMPTY_INPUT, + handler=lambda: {"value": 7}) + server = MCPServer(tools=[typed, plain]) + typed_result = server._handle_tools_call(2, {"name": "typed", + "arguments": {}}) + plain_result = server._handle_tools_call(3, {"name": "plain", + "arguments": {}}) + assert typed_result["isError"] is False + assert typed_result["structuredContent"] == {"value": 42} + assert "structuredContent" not in plain_result + + +def test_non_dict_result_has_no_structured_content(): + listy = MCPTool(name="listy", description="d", input_schema=_EMPTY_INPUT, + handler=lambda: [1, 2, 3], output_schema=_OUT) + server = MCPServer(tools=[listy]) + result = server._handle_tools_call(2, {"name": "listy", "arguments": {}}) + assert "structuredContent" not in result # only dict results qualify + + +def test_default_registry_tool_declares_output_schema(): + tool = {t.name: t for t in build_default_tool_registry()}["ac_validate_rows"] + assert tool.output_schema is not None + assert "ok" in tool.output_schema["properties"] + assert "outputSchema" in tool.to_descriptor()