server-management-lib 0.2.0__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.
@@ -0,0 +1,126 @@
1
+ """Structured tool responses shared by both MCP servers.
2
+
3
+ Tools keep returning a human-readable ``str`` so they stay easy to test and
4
+ log. The :func:`make_structured_tool` factory registers a second, MCP-facing
5
+ wrapper that returns :class:`ToolResponse`, which MCPServer exposes as
6
+ ``structuredContent`` next to the usual text block. Callers that only read the
7
+ text content therefore see no change, while clients that understand structured
8
+ output can consume the typed envelope (``ok``, ``tool``, ``summary``,
9
+ ``data``).
10
+
11
+ A tool can return :class:`ToolOutput` instead of a plain string when it has
12
+ machine-readable details to publish. The text wrapper unwraps it back to the
13
+ readable string.
14
+ """
15
+
16
+ import functools
17
+ import inspect
18
+ from collections.abc import Awaitable, Callable
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+ from pydantic import BaseModel, Field
23
+
24
+ # The markers the tools use for failures. ``⚠️`` is a warning that still carries
25
+ # a usable result (for example a skipped SMART self-test), so only ``❌`` marks
26
+ # a response as not ok.
27
+ ERROR_PREFIX = "❌"
28
+
29
+
30
+ class ToolResponse(BaseModel):
31
+ """Structured envelope returned by every MCP tool."""
32
+
33
+ ok: bool = Field(description="False when the tool reported an error")
34
+ tool: str = Field(description="Name of the tool that produced this response")
35
+ summary: str = Field(
36
+ default="", description="First line of the human-readable output"
37
+ )
38
+ text: str = Field(description="Human-readable output, identical to the text block")
39
+ data: dict[str, Any] = Field(
40
+ default_factory=dict,
41
+ description="Machine-readable details when the tool has structured data",
42
+ )
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class ToolOutput:
47
+ """Readable text plus optional structured details from a tool."""
48
+
49
+ text: str
50
+ ok: bool | None = None
51
+ summary: str = ""
52
+ data: dict[str, Any] = field(default_factory=dict)
53
+
54
+
55
+ ToolResult = str | ToolOutput
56
+
57
+
58
+ def _first_line(text: str) -> str:
59
+ for line in text.splitlines():
60
+ if line.strip():
61
+ return line.strip()
62
+ return ""
63
+
64
+
65
+ def _ok_from_text(text: str) -> bool:
66
+ return not text.lstrip().startswith(ERROR_PREFIX)
67
+
68
+
69
+ def as_text(result: ToolResult) -> str:
70
+ """Return the readable text of a tool result."""
71
+ return result.text if isinstance(result, ToolOutput) else result
72
+
73
+
74
+ def build_response(tool: str, result: ToolResult) -> ToolResponse:
75
+ """Build the structured envelope for a tool result."""
76
+ if isinstance(result, ToolOutput):
77
+ text = result.text
78
+ ok = _ok_from_text(text) if result.ok is None else result.ok
79
+ summary = result.summary or _first_line(text)
80
+ data = dict(result.data)
81
+ else:
82
+ text = result
83
+ ok = _ok_from_text(text)
84
+ summary = _first_line(text)
85
+ data = {}
86
+ return ToolResponse(ok=ok, tool=tool, summary=summary, text=text, data=data)
87
+
88
+
89
+ RegisterTool = Callable[[str, Callable[..., Awaitable[ToolResponse]], Any], None]
90
+ ToolDecorator = Callable[[Callable[..., Awaitable[ToolResult]]], Callable[..., Any]]
91
+
92
+
93
+ def make_structured_tool(register_tool: RegisterTool) -> Callable[[Any], ToolDecorator]:
94
+ """Return a ``@structured_tool(annotations)`` decorator for one MCP server.
95
+
96
+ ``register_tool(name, wrapper, annotations)`` is supplied by the server and
97
+ is responsible for handing the structured wrapper to MCPServer.
98
+ """
99
+
100
+ def structured_tool(annotations: Any) -> ToolDecorator:
101
+ def decorator(
102
+ fn: Callable[..., Awaitable[ToolResult]],
103
+ ) -> Callable[..., Any]:
104
+ tool_name = fn.__name__
105
+
106
+ async def structured_call(*args: Any, **kwargs: Any) -> ToolResponse:
107
+ return build_response(tool_name, await fn(*args, **kwargs))
108
+
109
+ structured_call.__name__ = tool_name
110
+ structured_call.__doc__ = fn.__doc__
111
+ # publish the ToolResponse output schema while keeping the original
112
+ # parameter annotations for the tool input schema
113
+ structured_call.__signature__ = inspect.signature(fn).replace( # type: ignore[attr-defined]
114
+ return_annotation=ToolResponse
115
+ )
116
+ register_tool(tool_name, structured_call, annotations)
117
+
118
+ @functools.wraps(fn)
119
+ async def text_call(*args: Any, **kwargs: Any) -> str:
120
+ return as_text(await fn(*args, **kwargs))
121
+
122
+ return text_call
123
+
124
+ return decorator
125
+
126
+ return structured_tool