holon-sh 0.0.1.dev1__tar.gz → 0.0.1.dev3__tar.gz
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_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/LICENSE +1 -1
- holon_sh-0.0.1.dev3/PKG-INFO +104 -0
- holon_sh-0.0.1.dev3/README.md +91 -0
- holon_sh-0.0.1.dev3/holon/binding.py +47 -0
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/holon/client.py +101 -47
- holon_sh-0.0.1.dev3/holon/view.py +55 -0
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/pyproject.toml +4 -2
- holon_sh-0.0.1.dev3/scripts/fixtures.sh +7 -0
- holon_sh-0.0.1.dev3/scripts/verify_distribution.py +37 -0
- holon_sh-0.0.1.dev3/tests/fixtures/caller/holon/apps/__init__.py +106 -0
- holon_sh-0.0.1.dev3/tests/fixtures/caller/requirements.txt +1 -0
- holon_sh-0.0.1.dev3/tests/fixtures/caller/server.json +5 -0
- holon_sh-0.0.1.dev3/tests/fixtures/orders/server.json +5 -0
- holon_sh-0.0.1.dev3/tests/fixtures/orders/server.py +57 -0
- holon_sh-0.0.1.dev3/tests/test_package.py +67 -0
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/tests/test_runtime.py +87 -121
- holon_sh-0.0.1.dev1/PKG-INFO +0 -11
- holon_sh-0.0.1.dev1/holon/binding.py +0 -30
- holon_sh-0.0.1.dev1/holon/view.py +0 -35
- holon_sh-0.0.1.dev1/scripts/verify_distribution.py +0 -91
- holon_sh-0.0.1.dev1/tests/test_package.py +0 -83
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/.gitignore +0 -0
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/holon/py.typed +0 -0
- {holon_sh-0.0.1.dev1 → holon_sh-0.0.1.dev3}/tests/test_view.py +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: holon-sh
|
|
3
|
+
Version: 0.0.1.dev3
|
|
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
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Holon Python SDK
|
|
15
|
+
|
|
16
|
+
[](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml)
|
|
17
|
+
[](https://pypi.org/project/holon-sh/)
|
|
18
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
|
|
21
|
+
This repository contains the Python runtime for [`holon`](https://github.com/holon-sh/cli).
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv add --prerelease allow holon-sh
|
|
27
|
+
uv add --dev holon-types
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`holon-types` is the `holon` CLI; it brings mypy, which `holon types check` runs from the project's environment.
|
|
31
|
+
|
|
32
|
+
## Invoking MCP Servers
|
|
33
|
+
|
|
34
|
+
The Holon Python SDK allows generation of type-safe MCP clients. See [Example](https://github.com/holon-sh/cli#example) for an explanation, or [Dependencies](https://github.com/holon-sh/cli#dependencies) to understand dependency resolution.
|
|
35
|
+
|
|
36
|
+
### Type-checking MCP invocations
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
uv run holon types check
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
<details open>
|
|
43
|
+
<summary><h3>Call a tool</h3></summary>
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from holon.apps import apps
|
|
47
|
+
|
|
48
|
+
async def greet(name: str) -> str:
|
|
49
|
+
greeting = await apps.bar.tools.greet({"timeout": 5.0}, name=name)
|
|
50
|
+
return greeting["result"]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Arguments are keywords, validated before sending; the result is validated against the output schema and returned.
|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<details open>
|
|
58
|
+
<summary><h3>Handle errors</h3></summary>
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from holon.client import ToolCallError
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
await apps.bar.tools.greet(name="world")
|
|
65
|
+
except ToolCallError as error:
|
|
66
|
+
error.code # "tool_error" or "input_required"
|
|
67
|
+
error.result # the original MCP result
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
</details>
|
|
71
|
+
|
|
72
|
+
<details open>
|
|
73
|
+
<summary><h3>Read the whole result</h3></summary>
|
|
74
|
+
|
|
75
|
+
`call_tool` returns the full MCP result instead of raising:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
result = await apps.bar.call_tool("greet", name="world")
|
|
79
|
+
if result.result_type == "input_required":
|
|
80
|
+
... # call again with input_responses and request_state
|
|
81
|
+
elif result.is_error:
|
|
82
|
+
... # inspect result.content
|
|
83
|
+
else:
|
|
84
|
+
result.structured_content # typed as Outputs.bar.greet
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The options dict also accepts `input_responses`, `request_state` and `meta`.
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
## Serve an [MCP App](https://modelcontextprotocol.io/extensions/apps/overview) view
|
|
92
|
+
|
|
93
|
+
`holon.view.app_view` registers a built HTML file as an MCP App resource:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from holon.view import app_view
|
|
97
|
+
|
|
98
|
+
server.add_resource(app_view(
|
|
99
|
+
"ui://foo/index.html", "Foo", "dist/index.html",
|
|
100
|
+
connect_domains=["https://bar.example.com"],
|
|
101
|
+
))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The page itself uses [`holon-ui`](https://github.com/holon-sh/sdk-ui).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Holon Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/holon-sh/)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
This repository contains the Python runtime for [`holon`](https://github.com/holon-sh/cli).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
uv add --prerelease allow holon-sh
|
|
14
|
+
uv add --dev holon-types
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`holon-types` is the `holon` CLI; it brings mypy, which `holon types check` runs from the project's environment.
|
|
18
|
+
|
|
19
|
+
## Invoking MCP Servers
|
|
20
|
+
|
|
21
|
+
The Holon Python SDK allows generation of type-safe MCP clients. See [Example](https://github.com/holon-sh/cli#example) for an explanation, or [Dependencies](https://github.com/holon-sh/cli#dependencies) to understand dependency resolution.
|
|
22
|
+
|
|
23
|
+
### Type-checking MCP invocations
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv run holon types check
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
<details open>
|
|
30
|
+
<summary><h3>Call a tool</h3></summary>
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from holon.apps import apps
|
|
34
|
+
|
|
35
|
+
async def greet(name: str) -> str:
|
|
36
|
+
greeting = await apps.bar.tools.greet({"timeout": 5.0}, name=name)
|
|
37
|
+
return greeting["result"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Arguments are keywords, validated before sending; the result is validated against the output schema and returned.
|
|
41
|
+
|
|
42
|
+
</details>
|
|
43
|
+
|
|
44
|
+
<details open>
|
|
45
|
+
<summary><h3>Handle errors</h3></summary>
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from holon.client import ToolCallError
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
await apps.bar.tools.greet(name="world")
|
|
52
|
+
except ToolCallError as error:
|
|
53
|
+
error.code # "tool_error" or "input_required"
|
|
54
|
+
error.result # the original MCP result
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
<details open>
|
|
60
|
+
<summary><h3>Read the whole result</h3></summary>
|
|
61
|
+
|
|
62
|
+
`call_tool` returns the full MCP result instead of raising:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
result = await apps.bar.call_tool("greet", name="world")
|
|
66
|
+
if result.result_type == "input_required":
|
|
67
|
+
... # call again with input_responses and request_state
|
|
68
|
+
elif result.is_error:
|
|
69
|
+
... # inspect result.content
|
|
70
|
+
else:
|
|
71
|
+
result.structured_content # typed as Outputs.bar.greet
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The options dict also accepts `input_responses`, `request_state` and `meta`.
|
|
75
|
+
|
|
76
|
+
</details>
|
|
77
|
+
|
|
78
|
+
## Serve an [MCP App](https://modelcontextprotocol.io/extensions/apps/overview) view
|
|
79
|
+
|
|
80
|
+
`holon.view.app_view` registers a built HTML file as an MCP App resource:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from holon.view import app_view
|
|
84
|
+
|
|
85
|
+
server.add_resource(app_view(
|
|
86
|
+
"ui://foo/index.html", "Foo", "dist/index.html",
|
|
87
|
+
connect_domains=["https://bar.example.com"],
|
|
88
|
+
))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The page itself uses [`holon-ui`](https://github.com/holon-sh/sdk-ui).
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
"""Connection settings for an MCP dependency.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
endpoint: Streamable HTTP MCP URL.
|
|
14
|
+
http_client: Factory returning a fresh ``httpx2.AsyncClient`` for each
|
|
15
|
+
call. Configure authentication headers or a custom transport here.
|
|
16
|
+
The SDK closes the returned client when the call finishes.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
endpoint: str
|
|
20
|
+
http_client: NotRequired[Callable[[], httpx2.AsyncClient]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_resolver: Callable[[str], Binding | None] | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_default_binding(resolve: Callable[[str], Binding | None]) -> None:
|
|
27
|
+
"""Register the default binding resolver for app clients.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
resolve: Process-wide callback receiving the dependency name. Return a
|
|
31
|
+
binding or ``None`` to use the environment fallback. Explicit
|
|
32
|
+
bindings take precedence. Register during application startup.
|
|
33
|
+
"""
|
|
34
|
+
global _resolver
|
|
35
|
+
_resolver = resolve
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_default_binding(app: str) -> Binding | None:
|
|
39
|
+
"""Return the registered resolver's binding for an app.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
app: Dependency name passed unchanged to the resolver.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The resolved binding, or ``None`` if none is available.
|
|
46
|
+
"""
|
|
47
|
+
return _resolver(app) if _resolver is not None else None
|
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from collections.abc import Mapping
|
|
5
|
-
from
|
|
5
|
+
from functools import partial
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any, Generic, Literal, Protocol, TypeAlias, TypedDict, TypeVar
|
|
6
8
|
|
|
7
9
|
import httpx2
|
|
8
10
|
import jsonschema # type: ignore[import-untyped,unused-ignore]
|
|
@@ -24,12 +26,14 @@ from referencing import Registry
|
|
|
24
26
|
from holon.binding import Binding, resolve_default_binding
|
|
25
27
|
|
|
26
28
|
# Binding is re-exported for the generated holon.apps module.
|
|
27
|
-
__all__ = ["Apps", "Binding", "ToolCallError", "ToolResult"]
|
|
29
|
+
__all__ = ["Apps", "Binding", "CallOptions", "ToolCallError", "ToolResult"]
|
|
28
30
|
|
|
29
31
|
Output = TypeVar("Output")
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class CompleteResult(Protocol, Generic[Output]):
|
|
35
|
+
"""Successful tool result with contract-typed structured content."""
|
|
36
|
+
|
|
33
37
|
result_type: Literal["complete"]
|
|
34
38
|
is_error: Literal[False]
|
|
35
39
|
structured_content: Output
|
|
@@ -38,6 +42,8 @@ class CompleteResult(Protocol, Generic[Output]):
|
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
class ErrorResult(Protocol):
|
|
45
|
+
"""Completed tool result reporting an application error."""
|
|
46
|
+
|
|
41
47
|
result_type: Literal["complete"]
|
|
42
48
|
is_error: Literal[True]
|
|
43
49
|
structured_content: object
|
|
@@ -50,7 +56,26 @@ ToolResult: TypeAlias = CompleteResult[Output] | ErrorResult | InputRequiredResu
|
|
|
50
56
|
_result = TypeAdapter(CallToolResult | InputRequiredResult)
|
|
51
57
|
|
|
52
58
|
|
|
59
|
+
class CallOptions(TypedDict, total=False):
|
|
60
|
+
"""Request controls, passed positionally to keep tool keywords available."""
|
|
61
|
+
|
|
62
|
+
timeout: float | None
|
|
63
|
+
input_responses: InputResponses | None
|
|
64
|
+
request_state: str | None
|
|
65
|
+
meta: RequestParamsMeta | None
|
|
66
|
+
|
|
67
|
+
|
|
53
68
|
class ToolCallError(RuntimeError):
|
|
69
|
+
"""Raised by ``tools.<name>`` for tool errors or additional input requests.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
app: Dependency name.
|
|
73
|
+
tool: Requested tool name.
|
|
74
|
+
result: Original MCP error or input-required result.
|
|
75
|
+
code: ``tool_error`` or ``input_required``. Use ``call_tool`` when you
|
|
76
|
+
need to inspect these results and submit additional input.
|
|
77
|
+
"""
|
|
78
|
+
|
|
54
79
|
def __init__(
|
|
55
80
|
self, app: str, tool: str, result: CallToolResult | InputRequiredResult
|
|
56
81
|
) -> None:
|
|
@@ -94,18 +119,41 @@ class _App:
|
|
|
94
119
|
if contract is None
|
|
95
120
|
else {tool["name"]: tool for tool in contract["tools"]}
|
|
96
121
|
)
|
|
97
|
-
self.tools =
|
|
122
|
+
self.tools = SimpleNamespace(
|
|
123
|
+
**{
|
|
124
|
+
name: partial(self._call_tool, name)
|
|
125
|
+
for name in self._definitions or ()
|
|
126
|
+
if not name.startswith("_")
|
|
127
|
+
}
|
|
128
|
+
)
|
|
98
129
|
|
|
99
130
|
async def call_tool(
|
|
100
131
|
self,
|
|
101
132
|
name: str,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
input_responses: InputResponses | None = None,
|
|
106
|
-
request_state: str | None = None,
|
|
107
|
-
meta: RequestParamsMeta | None = None,
|
|
133
|
+
options: CallOptions | None = None,
|
|
134
|
+
/,
|
|
135
|
+
**arguments: object,
|
|
108
136
|
) -> CallToolResult | InputRequiredResult:
|
|
137
|
+
"""Call a dependency tool and return its full MCP result.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
name: Tool name. Contracted dependencies reject unknown tools.
|
|
141
|
+
options: Request timeout in seconds, input responses, continuation
|
|
142
|
+
state, and metadata. Passed positionally, separate from tool input.
|
|
143
|
+
**arguments: Tool input, validated against the contract if present.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
A complete or input-required result. Tool error results are returned
|
|
147
|
+
unchanged; successful structured output is contract-validated.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: Unknown tool, invalid input/output, or undeclared dependency.
|
|
151
|
+
RuntimeError: Endpoint resolution failed.
|
|
152
|
+
|
|
153
|
+
Transport and protocol exceptions propagate. Each call closes its HTTP
|
|
154
|
+
client and MCP session, including when an exception occurs.
|
|
155
|
+
"""
|
|
156
|
+
options = options or {}
|
|
109
157
|
definition = None
|
|
110
158
|
if self._definitions is not None:
|
|
111
159
|
definition = self._definitions.get(name)
|
|
@@ -127,12 +175,10 @@ class _App:
|
|
|
127
175
|
raise RuntimeError(
|
|
128
176
|
f"Dependency {self._name} is unavailable ({variable}). Set the endpoint or supply a binding."
|
|
129
177
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
async with http:
|
|
178
|
+
http_client = partial(httpx2.AsyncClient, trust_env=False)
|
|
179
|
+
async with (
|
|
180
|
+
binding.get("http_client", http_client) if binding else http_client
|
|
181
|
+
)() as http:
|
|
136
182
|
transport = streamable_http_client(endpoint, http_client=http)
|
|
137
183
|
async with Client(
|
|
138
184
|
transport,
|
|
@@ -145,14 +191,14 @@ class _App:
|
|
|
145
191
|
CallToolRequest(
|
|
146
192
|
params=CallToolRequestParams(
|
|
147
193
|
name=name,
|
|
148
|
-
arguments=
|
|
149
|
-
input_responses=input_responses,
|
|
150
|
-
request_state=request_state,
|
|
151
|
-
_meta=meta,
|
|
194
|
+
arguments=arguments,
|
|
195
|
+
input_responses=options.get("input_responses"),
|
|
196
|
+
request_state=options.get("request_state"),
|
|
197
|
+
_meta=options.get("meta"),
|
|
152
198
|
)
|
|
153
199
|
),
|
|
154
200
|
_result,
|
|
155
|
-
request_read_timeout_seconds=timeout,
|
|
201
|
+
request_read_timeout_seconds=options.get("timeout"),
|
|
156
202
|
)
|
|
157
203
|
if (
|
|
158
204
|
definition is not None
|
|
@@ -167,28 +213,41 @@ class _App:
|
|
|
167
213
|
)
|
|
168
214
|
return result
|
|
169
215
|
|
|
216
|
+
async def _call_tool(
|
|
217
|
+
self,
|
|
218
|
+
name: str,
|
|
219
|
+
options: CallOptions | None = None,
|
|
220
|
+
/,
|
|
221
|
+
**arguments: object,
|
|
222
|
+
) -> object:
|
|
223
|
+
result = await self.call_tool(name, options, **arguments)
|
|
224
|
+
if isinstance(result, InputRequiredResult) or result.is_error:
|
|
225
|
+
raise ToolCallError(self._name, name, result)
|
|
226
|
+
return result.structured_content
|
|
227
|
+
|
|
170
228
|
|
|
171
|
-
class
|
|
172
|
-
|
|
173
|
-
self._client = client
|
|
229
|
+
class Apps:
|
|
230
|
+
"""Create the dependency collection used by generated Python clients.
|
|
174
231
|
|
|
175
|
-
|
|
176
|
-
definitions = self._client._definitions
|
|
177
|
-
if name.startswith("_") or definitions is None or name not in definitions:
|
|
178
|
-
raise AttributeError(name)
|
|
232
|
+
Applications typically use ``apps`` or ``create_apps`` from ``holon.apps``.
|
|
179
233
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
234
|
+
Args:
|
|
235
|
+
identity: Application name, version, and declared ``requires`` mapping.
|
|
236
|
+
contracts: Dependency contracts with a ``tools`` list, or ``None`` for
|
|
237
|
+
untyped dependencies.
|
|
238
|
+
bindings: Explicit bindings. When supplied, only these dependencies are
|
|
239
|
+
included; an empty mapping selects none. Otherwise all declared
|
|
240
|
+
dependencies and contracts are available.
|
|
187
241
|
|
|
188
|
-
|
|
242
|
+
Raises:
|
|
243
|
+
ValueError: A binding names an unknown dependency.
|
|
189
244
|
|
|
245
|
+
Access a client with ``clients[name]`` or ``clients.name``. Its
|
|
246
|
+
``tools.<name>(**arguments)`` methods return structured output
|
|
247
|
+
and raise :class:`ToolCallError` for tool errors or additional input requests.
|
|
248
|
+
Use its ``call_tool`` method for the full result envelope.
|
|
249
|
+
"""
|
|
190
250
|
|
|
191
|
-
class Apps:
|
|
192
251
|
def __init__(
|
|
193
252
|
self,
|
|
194
253
|
identity: dict[str, Any],
|
|
@@ -196,19 +255,14 @@ class Apps:
|
|
|
196
255
|
bindings: Mapping[str, Binding] | None = None,
|
|
197
256
|
) -> None:
|
|
198
257
|
# Every declared dependency gets a client; those without a contract are untyped.
|
|
199
|
-
available
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
available.update(contracts)
|
|
203
|
-
selected = available if bindings is None else {}
|
|
204
|
-
for name in bindings or ():
|
|
258
|
+
available = dict.fromkeys(identity["requires"]) | contracts
|
|
259
|
+
self._apps = {}
|
|
260
|
+
for name in available if bindings is None else bindings:
|
|
205
261
|
if name not in available:
|
|
206
262
|
raise ValueError(f"Unknown app binding: {name}")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
for name, contract in selected.items()
|
|
211
|
-
}
|
|
263
|
+
self._apps[name] = _App(
|
|
264
|
+
name, identity, available[name], bindings[name] if bindings else None
|
|
265
|
+
)
|
|
212
266
|
|
|
213
267
|
def __getitem__(self, name: str) -> _App:
|
|
214
268
|
return self._apps[name]
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
"""Create a server resource serving a built MCP App HTML file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
uri: Resource URI, normally starting with ``ui://``.
|
|
21
|
+
name: Human-readable resource name.
|
|
22
|
+
file: HTML file to read lazily on the first resource request.
|
|
23
|
+
connect_domains: Domains allowed for network connections by the view.
|
|
24
|
+
resource_domains: Domains allowed for external view assets.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A ``FunctionResource`` ready for the MCP server's resource manager.
|
|
28
|
+
Successful reads are cached for the lifetime of this resource instance.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
OSError: Reading the HTML file failed.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
``resource = app_view("ui://app/index.html", "App", "dist/index.html")``
|
|
35
|
+
"""
|
|
36
|
+
page = Path(file)
|
|
37
|
+
csp = {
|
|
38
|
+
"connectDomains": connect_domains or [],
|
|
39
|
+
"resourceDomains": resource_domains or [],
|
|
40
|
+
}
|
|
41
|
+
built: list[str] = []
|
|
42
|
+
|
|
43
|
+
def read() -> str:
|
|
44
|
+
# Concurrent first reads must all return the first cached build.
|
|
45
|
+
if not built:
|
|
46
|
+
built.append(page.read_text())
|
|
47
|
+
return built[0]
|
|
48
|
+
|
|
49
|
+
return FunctionResource.from_function(
|
|
50
|
+
read,
|
|
51
|
+
uri,
|
|
52
|
+
name=name,
|
|
53
|
+
mime_type=APP_MIME_TYPE,
|
|
54
|
+
meta={"ui": {"csp": csp}},
|
|
55
|
+
)
|
|
@@ -4,8 +4,9 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "holon-sh"
|
|
7
|
-
version = "0.0.1.
|
|
7
|
+
version = "0.0.1.dev3"
|
|
8
8
|
description = "Typed MCP clients, bindings, and app views"
|
|
9
|
+
readme = "README.md"
|
|
9
10
|
license = "MIT"
|
|
10
11
|
license-files = ["LICENSE"]
|
|
11
12
|
requires-python = ">=3.11"
|
|
@@ -25,6 +26,7 @@ testpaths = ["tests"]
|
|
|
25
26
|
|
|
26
27
|
[tool.ruff]
|
|
27
28
|
target-version = "py311"
|
|
29
|
+
extend-exclude = ["tests/fixtures"]
|
|
28
30
|
|
|
29
31
|
[dependency-groups]
|
|
30
|
-
dev = ["
|
|
32
|
+
dev = ["mypy==2.4.0", "pytest==9.1.1", "ruff==0.16.8", "types-jsonschema", "hatchling>=1.27,<2"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
# Run from any directory with the holon CLI on PATH. The orders server is
|
|
5
|
+
# statically extracted; no running server or installed fixture dependencies are needed.
|
|
6
|
+
cd "$(dirname "$0")/../tests/fixtures/caller"
|
|
7
|
+
exec holon types generate -s ../orders
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Install and test each built distribution in an isolated uv environment."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
root = Path(__file__).resolve().parents[1]
|
|
9
|
+
artifacts = sorted(root.glob("dist/holon_sh-*.whl")) + sorted(
|
|
10
|
+
root.glob("dist/holon_sh-*.tar.gz")
|
|
11
|
+
)
|
|
12
|
+
if len(artifacts) != 2:
|
|
13
|
+
raise RuntimeError("Build exactly one wheel and one sdist before verification.")
|
|
14
|
+
for artifact in artifacts:
|
|
15
|
+
subprocess.run(
|
|
16
|
+
[
|
|
17
|
+
"uv",
|
|
18
|
+
"run",
|
|
19
|
+
"--isolated",
|
|
20
|
+
"--no-project",
|
|
21
|
+
"--no-config",
|
|
22
|
+
"--python",
|
|
23
|
+
sys.executable,
|
|
24
|
+
"--with",
|
|
25
|
+
str(artifact),
|
|
26
|
+
"--with",
|
|
27
|
+
f"pytest=={version('pytest')}",
|
|
28
|
+
"--with",
|
|
29
|
+
f"mypy=={version('mypy')}",
|
|
30
|
+
"python",
|
|
31
|
+
"-m",
|
|
32
|
+
"pytest",
|
|
33
|
+
str(root / "tests/test_package.py"),
|
|
34
|
+
],
|
|
35
|
+
cwd=root / "dist",
|
|
36
|
+
check=True,
|
|
37
|
+
)
|