holon-sh 0.0.1.dev1__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.
holon/binding.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import NotRequired, TypedDict
5
+
6
+ import httpx2
7
+
8
+
9
+ class Binding(TypedDict):
10
+ endpoint: str
11
+ http_client: NotRequired[Callable[[], httpx2.AsyncClient]]
12
+ # Python clients open one session per call, so every binding already behaves as scoped.
13
+ scoped: NotRequired[bool]
14
+
15
+
16
+ _resolver: Callable[[str], Binding | None] | None = None
17
+
18
+
19
+ def set_default_binding(resolve: Callable[[str], Binding | None]) -> None:
20
+ """Resolve bindings for app clients that were not bound with ``create_apps``.
21
+
22
+ Without a resolver, or when it returns ``None``, a client falls back to its
23
+ ``<NAME>_MCP_URL`` environment variable.
24
+ """
25
+ global _resolver
26
+ _resolver = resolve
27
+
28
+
29
+ def resolve_default_binding(app: str) -> Binding | None:
30
+ return _resolver(app) if _resolver is not None else None
holon/client.py ADDED
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Mapping
5
+ from typing import Any, Generic, Literal, Protocol, TypeAlias, TypeVar
6
+
7
+ import httpx2
8
+ import jsonschema # type: ignore[import-untyped,unused-ignore]
9
+ from mcp import Client
10
+ from mcp.client.streamable_http import streamable_http_client
11
+ from mcp.types import (
12
+ CallToolRequest,
13
+ CallToolRequestParams,
14
+ CallToolResult,
15
+ ContentBlock,
16
+ Implementation,
17
+ InputRequiredResult,
18
+ InputResponses,
19
+ RequestParamsMeta,
20
+ )
21
+ from pydantic import TypeAdapter
22
+ from referencing import Registry
23
+
24
+ from holon.binding import Binding, resolve_default_binding
25
+
26
+ # Binding is re-exported for the generated holon.apps module.
27
+ __all__ = ["Apps", "Binding", "ToolCallError", "ToolResult"]
28
+
29
+ Output = TypeVar("Output")
30
+
31
+
32
+ class CompleteResult(Protocol, Generic[Output]):
33
+ result_type: Literal["complete"]
34
+ is_error: Literal[False]
35
+ structured_content: Output
36
+ content: list[ContentBlock]
37
+ meta: dict[str, object] | None
38
+
39
+
40
+ class ErrorResult(Protocol):
41
+ result_type: Literal["complete"]
42
+ is_error: Literal[True]
43
+ structured_content: object
44
+ content: list[ContentBlock]
45
+ meta: dict[str, object] | None
46
+
47
+
48
+ ToolResult: TypeAlias = CompleteResult[Output] | ErrorResult | InputRequiredResult
49
+
50
+ _result = TypeAdapter(CallToolResult | InputRequiredResult)
51
+
52
+
53
+ class ToolCallError(RuntimeError):
54
+ def __init__(
55
+ self, app: str, tool: str, result: CallToolResult | InputRequiredResult
56
+ ) -> None:
57
+ self.app = app
58
+ self.tool = tool
59
+ self.result = result
60
+ self.code: Literal["tool_error", "input_required"]
61
+ if isinstance(result, InputRequiredResult):
62
+ self.code, message = "input_required", "additional input required"
63
+ else:
64
+ self.code, message = "tool_error", "tool failed"
65
+ super().__init__(f"{app}.{tool}: {message}.")
66
+
67
+
68
+ def validate(schema: dict[str, Any], value: object, label: str) -> None:
69
+ try:
70
+ jsonschema.validate(
71
+ value,
72
+ schema,
73
+ registry=Registry(),
74
+ format_checker=jsonschema.FormatChecker(),
75
+ )
76
+ except jsonschema.ValidationError as error:
77
+ raise ValueError(f"Invalid {label}: {error.message}") from error
78
+
79
+
80
+ class _App:
81
+ def __init__(
82
+ self,
83
+ name: str,
84
+ identity: dict[str, Any],
85
+ contract: dict[str, Any] | None,
86
+ binding: Binding | None = None,
87
+ ) -> None:
88
+ self._name = name
89
+ self._identity = identity
90
+ self._binding = binding
91
+ # None for a dependency without a contract: any tool, unvalidated.
92
+ self._definitions = (
93
+ None
94
+ if contract is None
95
+ else {tool["name"]: tool for tool in contract["tools"]}
96
+ )
97
+ self.tools = _Tools(self)
98
+
99
+ async def call_tool(
100
+ self,
101
+ name: str,
102
+ arguments: Mapping[str, object],
103
+ *,
104
+ timeout: float | None = None,
105
+ input_responses: InputResponses | None = None,
106
+ request_state: str | None = None,
107
+ meta: RequestParamsMeta | None = None,
108
+ ) -> CallToolResult | InputRequiredResult:
109
+ definition = None
110
+ if self._definitions is not None:
111
+ definition = self._definitions.get(name)
112
+ if definition is None:
113
+ raise ValueError(f"Unknown tool {self._name}.{name}.")
114
+ validate(
115
+ definition["inputSchema"],
116
+ arguments,
117
+ f"arguments for {self._name}.{name}",
118
+ )
119
+ binding = self._binding or resolve_default_binding(self._name)
120
+ if binding is None and self._name not in self._identity["requires"]:
121
+ raise ValueError(
122
+ f"Dependency {self._name} is not declared by this application."
123
+ )
124
+ variable = self._name.upper().replace("-", "_") + "_MCP_URL"
125
+ endpoint = binding["endpoint"] if binding else os.environ.get(variable)
126
+ if not endpoint:
127
+ raise RuntimeError(
128
+ f"Dependency {self._name} is unavailable ({variable}). Set the endpoint or supply a binding."
129
+ )
130
+ http = (
131
+ binding["http_client"]()
132
+ if binding and "http_client" in binding
133
+ else httpx2.AsyncClient(trust_env=False)
134
+ )
135
+ async with http:
136
+ transport = streamable_http_client(endpoint, http_client=http)
137
+ async with Client(
138
+ transport,
139
+ client_info=Implementation(
140
+ name=self._identity["name"], version=self._identity["version"]
141
+ ),
142
+ mode="2026-07-28",
143
+ ) as client:
144
+ result = await client.session.send_request(
145
+ CallToolRequest(
146
+ params=CallToolRequestParams(
147
+ name=name,
148
+ arguments=dict(arguments),
149
+ input_responses=input_responses,
150
+ request_state=request_state,
151
+ _meta=meta,
152
+ )
153
+ ),
154
+ _result,
155
+ request_read_timeout_seconds=timeout,
156
+ )
157
+ if (
158
+ definition is not None
159
+ and isinstance(result, CallToolResult)
160
+ and not result.is_error
161
+ and "outputSchema" in definition
162
+ ):
163
+ validate(
164
+ definition["outputSchema"],
165
+ result.structured_content,
166
+ f"structured output from {self._name}.{name}",
167
+ )
168
+ return result
169
+
170
+
171
+ class _Tools:
172
+ def __init__(self, client: _App) -> None:
173
+ self._client = client
174
+
175
+ def __getattr__(self, name: str) -> Any:
176
+ definitions = self._client._definitions
177
+ if name.startswith("_") or definitions is None or name not in definitions:
178
+ raise AttributeError(name)
179
+
180
+ async def call(
181
+ arguments: Mapping[str, object], *, timeout: float | None = None
182
+ ) -> object:
183
+ result = await self._client.call_tool(name, arguments, timeout=timeout)
184
+ if isinstance(result, InputRequiredResult) or result.is_error:
185
+ raise ToolCallError(self._client._name, name, result)
186
+ return result.structured_content
187
+
188
+ return call
189
+
190
+
191
+ class Apps:
192
+ def __init__(
193
+ self,
194
+ identity: dict[str, Any],
195
+ contracts: dict[str, Any],
196
+ bindings: Mapping[str, Binding] | None = None,
197
+ ) -> None:
198
+ # Every declared dependency gets a client; those without a contract are untyped.
199
+ available: dict[str, dict[str, Any] | None] = {
200
+ name: None for name in identity["requires"]
201
+ }
202
+ available.update(contracts)
203
+ selected = available if bindings is None else {}
204
+ for name in bindings or ():
205
+ if name not in available:
206
+ raise ValueError(f"Unknown app binding: {name}")
207
+ selected[name] = available[name]
208
+ self._apps = {
209
+ name: _App(name, identity, contract, bindings[name] if bindings else None)
210
+ for name, contract in selected.items()
211
+ }
212
+
213
+ def __getitem__(self, name: str) -> _App:
214
+ return self._apps[name]
215
+
216
+ def __getattr__(self, name: str) -> _App:
217
+ if name.startswith("_") or name not in self._apps:
218
+ raise AttributeError(name)
219
+ return self._apps[name]
holon/py.typed ADDED
File without changes
holon/view.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from mcp.server.apps import APP_MIME_TYPE
6
+ from mcp.server.mcpserver.resources import FunctionResource
7
+
8
+
9
+ def app_view(
10
+ uri: str,
11
+ name: str,
12
+ file: Path | str,
13
+ *,
14
+ connect_domains: list[str] | None = None,
15
+ resource_domains: list[str] | None = None,
16
+ ) -> FunctionResource:
17
+ page = Path(file)
18
+ csp = {
19
+ "connectDomains": connect_domains or [],
20
+ "resourceDomains": resource_domains or [],
21
+ }
22
+ built: list[str] = []
23
+
24
+ def read() -> str:
25
+ if not built:
26
+ built.append(page.read_text())
27
+ return built[0]
28
+
29
+ return FunctionResource.from_function(
30
+ read,
31
+ uri,
32
+ name=name,
33
+ mime_type=APP_MIME_TYPE,
34
+ meta={"ui": {"csp": csp}},
35
+ )
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: holon-sh
3
+ Version: 0.0.1.dev1
4
+ Summary: Typed MCP clients, bindings, and app views
5
+ Project-URL: Repository, https://github.com/holon-sh/sdk-py
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: httpx2<3,>=2.10
10
+ Requires-Dist: jsonschema<5,>=4.26
11
+ Requires-Dist: mcp<3,>=2.3
@@ -0,0 +1,8 @@
1
+ holon/binding.py,sha256=ulQQIGyA80wyzAvcosArFQMXg77-HG-0LwXPahFW878,878
2
+ holon/client.py,sha256=S-HwG4A-lUbY93jBe8ZktcibFK1Ss-0rzWCzQfEaJJQ,7464
3
+ holon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ holon/view.py,sha256=SuXo3cBow23OtrJaI9dF5hlptxh2NmgnbAozWTg5fRQ,797
5
+ holon_sh-0.0.1.dev1.dist-info/METADATA,sha256=y5rVgIUZ3uxx5Mz2UrdSsjS3UnbQd6FXYoe1bsbEMfk,332
6
+ holon_sh-0.0.1.dev1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
7
+ holon_sh-0.0.1.dev1.dist-info/licenses/LICENSE,sha256=h2vZ3IxSxiusmhPhiF-u5T3DkFywBJ3ayE0P2LpI_tQ,1067
8
+ holon_sh-0.0.1.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holon Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.