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.
- server_management_lib/__init__.py +124 -0
- server_management_lib/audit.py +80 -0
- server_management_lib/base.py +55 -0
- server_management_lib/config.py +385 -0
- server_management_lib/executor.py +53 -0
- server_management_lib/http_clients.py +636 -0
- server_management_lib/local_executor.py +200 -0
- server_management_lib/operations.py +33 -0
- server_management_lib/output.py +61 -0
- server_management_lib/py.typed +0 -0
- server_management_lib/rate_limit.py +118 -0
- server_management_lib/redaction.py +191 -0
- server_management_lib/responses.py +126 -0
- server_management_lib/security.py +1552 -0
- server_management_lib/ssh_manager.py +447 -0
- server_management_lib/transport.py +128 -0
- server_management_lib/tunnel.py +380 -0
- server_management_lib-0.2.0.dist-info/METADATA +46 -0
- server_management_lib-0.2.0.dist-info/RECORD +21 -0
- server_management_lib-0.2.0.dist-info/WHEEL +5 -0
- server_management_lib-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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
|