holon-sh 0.0.1.dev2__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.
Files changed (24) hide show
  1. holon_sh-0.0.1.dev3/PKG-INFO +104 -0
  2. holon_sh-0.0.1.dev3/README.md +91 -0
  3. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/holon/binding.py +0 -4
  4. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/holon/client.py +28 -23
  5. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/pyproject.toml +1 -1
  6. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/scripts/fixtures.sh +1 -1
  7. holon_sh-0.0.1.dev3/tests/fixtures/caller/holon/apps/__init__.py +106 -0
  8. holon_sh-0.0.1.dev3/tests/fixtures/orders/server.py +57 -0
  9. holon_sh-0.0.1.dev3/tests/test_package.py +67 -0
  10. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/tests/test_runtime.py +46 -16
  11. holon_sh-0.0.1.dev2/PKG-INFO +0 -140
  12. holon_sh-0.0.1.dev2/README.md +0 -127
  13. holon_sh-0.0.1.dev2/tests/fixtures/caller/holon/apps/__init__.py +0 -55
  14. holon_sh-0.0.1.dev2/tests/fixtures/orders/server.py +0 -14
  15. holon_sh-0.0.1.dev2/tests/test_package.py +0 -40
  16. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/.gitignore +0 -0
  17. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/LICENSE +0 -0
  18. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/holon/py.typed +0 -0
  19. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/holon/view.py +0 -0
  20. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/scripts/verify_distribution.py +0 -0
  21. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/tests/fixtures/caller/requirements.txt +0 -0
  22. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/tests/fixtures/caller/server.json +0 -0
  23. {holon_sh-0.0.1.dev2 → holon_sh-0.0.1.dev3}/tests/fixtures/orders/server.json +0 -0
  24. {holon_sh-0.0.1.dev2 → 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
+ [![CI](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml)
17
+ [![PyPI](https://img.shields.io/pypi/v/holon-sh)](https://pypi.org/project/holon-sh/)
18
+ [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
19
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](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
+ [![CI](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/holon-sh/sdk-py/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/holon-sh)](https://pypi.org/project/holon-sh/)
5
+ [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
6
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](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).
@@ -14,14 +14,10 @@ class Binding(TypedDict):
14
14
  http_client: Factory returning a fresh ``httpx2.AsyncClient`` for each
15
15
  call. Configure authentication headers or a custom transport here.
16
16
  The SDK closes the returned client when the call finishes.
17
- scoped: Compatibility setting. Python always opens and closes a session
18
- per call, regardless of this value.
19
17
  """
20
18
 
21
19
  endpoint: str
22
20
  http_client: NotRequired[Callable[[], httpx2.AsyncClient]]
23
- # Python clients open one session per call, so every binding already behaves as scoped.
24
- scoped: NotRequired[bool]
25
21
 
26
22
 
27
23
  _resolver: Callable[[str], Binding | None] | None = None
@@ -4,7 +4,7 @@ import os
4
4
  from collections.abc import Mapping
5
5
  from functools import partial
6
6
  from types import SimpleNamespace
7
- from typing import Any, Generic, Literal, Protocol, TypeAlias, TypeVar
7
+ from typing import Any, Generic, Literal, Protocol, TypeAlias, TypedDict, TypeVar
8
8
 
9
9
  import httpx2
10
10
  import jsonschema # type: ignore[import-untyped,unused-ignore]
@@ -26,7 +26,7 @@ from referencing import Registry
26
26
  from holon.binding import Binding, resolve_default_binding
27
27
 
28
28
  # Binding is re-exported for the generated holon.apps module.
29
- __all__ = ["Apps", "Binding", "ToolCallError", "ToolResult"]
29
+ __all__ = ["Apps", "Binding", "CallOptions", "ToolCallError", "ToolResult"]
30
30
 
31
31
  Output = TypeVar("Output")
32
32
 
@@ -56,6 +56,15 @@ ToolResult: TypeAlias = CompleteResult[Output] | ErrorResult | InputRequiredResu
56
56
  _result = TypeAdapter(CallToolResult | InputRequiredResult)
57
57
 
58
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
+
59
68
  class ToolCallError(RuntimeError):
60
69
  """Raised by ``tools.<name>`` for tool errors or additional input requests.
61
70
 
@@ -121,22 +130,17 @@ class _App:
121
130
  async def call_tool(
122
131
  self,
123
132
  name: str,
124
- arguments: Mapping[str, object],
125
- *,
126
- timeout: float | None = None,
127
- input_responses: InputResponses | None = None,
128
- request_state: str | None = None,
129
- meta: RequestParamsMeta | None = None,
133
+ options: CallOptions | None = None,
134
+ /,
135
+ **arguments: object,
130
136
  ) -> CallToolResult | InputRequiredResult:
131
137
  """Call a dependency tool and return its full MCP result.
132
138
 
133
139
  Args:
134
140
  name: Tool name. Contracted dependencies reject unknown tools.
135
- arguments: Tool arguments, validated against the contract if present.
136
- timeout: Request read timeout in seconds, or the MCP client default.
137
- input_responses: Responses to a previous input-required result.
138
- request_state: Continuation state returned by the server.
139
- meta: Optional MCP request metadata.
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.
140
144
 
141
145
  Returns:
142
146
  A complete or input-required result. Tool error results are returned
@@ -149,6 +153,7 @@ class _App:
149
153
  Transport and protocol exceptions propagate. Each call closes its HTTP
150
154
  client and MCP session, including when an exception occurs.
151
155
  """
156
+ options = options or {}
152
157
  definition = None
153
158
  if self._definitions is not None:
154
159
  definition = self._definitions.get(name)
@@ -186,14 +191,14 @@ class _App:
186
191
  CallToolRequest(
187
192
  params=CallToolRequestParams(
188
193
  name=name,
189
- arguments=dict(arguments),
190
- input_responses=input_responses,
191
- request_state=request_state,
192
- _meta=meta,
194
+ arguments=arguments,
195
+ input_responses=options.get("input_responses"),
196
+ request_state=options.get("request_state"),
197
+ _meta=options.get("meta"),
193
198
  )
194
199
  ),
195
200
  _result,
196
- request_read_timeout_seconds=timeout,
201
+ request_read_timeout_seconds=options.get("timeout"),
197
202
  )
198
203
  if (
199
204
  definition is not None
@@ -211,11 +216,11 @@ class _App:
211
216
  async def _call_tool(
212
217
  self,
213
218
  name: str,
214
- arguments: Mapping[str, object],
215
- *,
216
- timeout: float | None = None,
219
+ options: CallOptions | None = None,
220
+ /,
221
+ **arguments: object,
217
222
  ) -> object:
218
- result = await self.call_tool(name, arguments, timeout=timeout)
223
+ result = await self.call_tool(name, options, **arguments)
219
224
  if isinstance(result, InputRequiredResult) or result.is_error:
220
225
  raise ToolCallError(self._name, name, result)
221
226
  return result.structured_content
@@ -238,7 +243,7 @@ class Apps:
238
243
  ValueError: A binding names an unknown dependency.
239
244
 
240
245
  Access a client with ``clients[name]`` or ``clients.name``. Its
241
- ``tools.<name>(arguments, timeout=seconds)`` methods return structured output
246
+ ``tools.<name>(**arguments)`` methods return structured output
242
247
  and raise :class:`ToolCallError` for tool errors or additional input requests.
243
248
  Use its ``call_tool`` method for the full result envelope.
244
249
  """
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "holon-sh"
7
- version = "0.0.1.dev2"
7
+ version = "0.0.1.dev3"
8
8
  description = "Typed MCP clients, bindings, and app views"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,4 +4,4 @@ set -eu
4
4
  # Run from any directory with the holon CLI on PATH. The orders server is
5
5
  # statically extracted; no running server or installed fixture dependencies are needed.
6
6
  cd "$(dirname "$0")/../tests/fixtures/caller"
7
- exec holon types generate ../orders
7
+ exec holon types generate -s ../orders
@@ -0,0 +1,106 @@
1
+ # holon-generated-apps-v1 app=caller
2
+ # Input alternatives share a result type; redundant overloads are harmless.
3
+ # mypy: disable-error-code=overload-cannot-match
4
+ from __future__ import annotations
5
+
6
+ import json as _json
7
+
8
+ from holon.client import Apps as _Apps
9
+ from holon.client import Binding as _Binding
10
+ from holon.client import CallOptions as _CallOptions
11
+ from holon.client import ToolResult as _ToolResult
12
+
13
+ import builtins as _builtins
14
+ import typing as _typing
15
+
16
+ OrdersFindInput = _typing.TypedDict("OrdersFindInput", {"arguments": _typing.NotRequired[_builtins.str], "class": _typing.NotRequired[_builtins.str], "id": _builtins.str, "limit": _typing.NotRequired[_builtins.int], "name": _typing.NotRequired[_builtins.str], "options": _typing.NotRequired[_builtins.str], "order-id": _typing.NotRequired[_builtins.str], "self": _typing.NotRequired[_builtins.str], "timeout": _typing.NotRequired[_builtins.str]})
17
+
18
+ OrdersFindInput2 = _typing.TypedDict("OrdersFindInput2", {"arguments": _typing.NotRequired[_builtins.str], "class": _typing.NotRequired[_builtins.str], "id": _builtins.str, "limit": _typing.NotRequired[_builtins.int], "name": _typing.NotRequired[_builtins.str], "options": _typing.NotRequired[_builtins.str], "order-id": _typing.NotRequired[_builtins.str], "self": _typing.NotRequired[_builtins.str], "timeout": _typing.NotRequired[_builtins.str]})
19
+
20
+ OrdersFindInput3 = _typing.TypedDict("OrdersFindInput3", {"email": _builtins.str})
21
+
22
+ OrdersGetOrderInput = _typing.TypedDict("OrdersGetOrderInput", {"arguments": _typing.NotRequired[_builtins.str], "class": _typing.NotRequired[_builtins.str], "id": _builtins.str, "limit": _typing.NotRequired[_builtins.int], "name": _typing.NotRequired[_builtins.str], "options": _typing.NotRequired[_builtins.str], "order-id": _typing.NotRequired[_builtins.str], "self": _typing.NotRequired[_builtins.str], "timeout": _typing.NotRequired[_builtins.str]})
23
+
24
+ OrdersGetOrderOutput = _typing.TypedDict("OrdersGetOrderOutput", {"total": _builtins.float})
25
+
26
+ OrdersPingInput = _typing.TypedDict("OrdersPingInput", {})
27
+
28
+ OrdersReadInput = _typing.TypedDict("OrdersReadInput", {"arguments": _typing.NotRequired[_builtins.str], "class": _typing.NotRequired[_builtins.str], "id": _builtins.str, "limit": _typing.NotRequired[_builtins.int], "name": _typing.NotRequired[_builtins.str], "options": _typing.NotRequired[_builtins.str], "order-id": _typing.NotRequired[_builtins.str], "self": _typing.NotRequired[_builtins.str], "timeout": _typing.NotRequired[_builtins.str]})
29
+
30
+ OrdersReadOutput = _typing.TypedDict("OrdersReadOutput", {"total": _builtins.float})
31
+
32
+ class Inputs:
33
+ class orders:
34
+ counts: _typing.TypeAlias = _builtins.dict[_builtins.str, _builtins.int]
35
+ find: _typing.TypeAlias = OrdersFindInput | OrdersFindInput2 | OrdersFindInput3
36
+ ping: _typing.TypeAlias = OrdersPingInput
37
+ read: _typing.TypeAlias = OrdersReadInput
38
+
39
+ class Outputs:
40
+ class orders:
41
+ counts: _typing.TypeAlias = _builtins.object
42
+ find: _typing.TypeAlias = _builtins.object
43
+ ping: _typing.TypeAlias = _builtins.object
44
+ read: _typing.TypeAlias = OrdersReadOutput
45
+
46
+ class InputTypes:
47
+ class orders:
48
+ class counts:
49
+ pass
50
+ class find:
51
+ pass
52
+ class ping:
53
+ pass
54
+ class read:
55
+ pass
56
+
57
+ class OutputTypes:
58
+ class orders:
59
+ class counts:
60
+ pass
61
+ class find:
62
+ pass
63
+ class ping:
64
+ pass
65
+ class read:
66
+ pass
67
+
68
+ class OrdersTools(_typing.Protocol):
69
+ async def counts(self, options: _CallOptions | None = ..., /, **arguments: _builtins.int) -> _builtins.object: ...
70
+ @_typing.overload
71
+ async def find(self, options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput]) -> _builtins.object: ...
72
+ @_typing.overload
73
+ async def find(self, options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput2]) -> _builtins.object: ...
74
+ @_typing.overload
75
+ async def find(self, options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput3]) -> _builtins.object: ...
76
+ async def ping(self, options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersPingInput]) -> _builtins.object: ...
77
+ async def read(self, options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersReadInput]) -> OrdersReadOutput: ...
78
+
79
+ class OrdersApp(_typing.Protocol):
80
+ tools: OrdersTools
81
+ @_typing.overload
82
+ async def call_tool(self, name: _typing.Literal["counts"], options: _CallOptions | None = ..., /, **arguments: _builtins.int) -> _ToolResult[_builtins.object]: ...
83
+ @_typing.overload
84
+ async def call_tool(self, name: _typing.Literal["find"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput]) -> _ToolResult[_builtins.object]: ...
85
+ @_typing.overload
86
+ async def call_tool(self, name: _typing.Literal["find"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput2]) -> _ToolResult[_builtins.object]: ...
87
+ @_typing.overload
88
+ async def call_tool(self, name: _typing.Literal["find"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersFindInput3]) -> _ToolResult[_builtins.object]: ...
89
+ @_typing.overload
90
+ async def call_tool(self, name: _typing.Literal["get-order"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersGetOrderInput]) -> _ToolResult[OrdersGetOrderOutput]: ...
91
+ @_typing.overload
92
+ async def call_tool(self, name: _typing.Literal["ping"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersPingInput]) -> _ToolResult[_builtins.object]: ...
93
+ @_typing.overload
94
+ async def call_tool(self, name: _typing.Literal["read"], options: _CallOptions | None = ..., /, **arguments: _typing.Unpack[OrdersReadInput]) -> _ToolResult[OrdersReadOutput]: ...
95
+
96
+ class _Clients(_typing.Protocol):
97
+ orders: OrdersApp
98
+ def __getitem__(self, name: _typing.Literal["orders"]) -> OrdersApp: ...
99
+
100
+
101
+ _identity, _contracts = _json.loads("[{\"name\":\"fixtures/caller\",\"requires\":{\"orders\":{\"name\":\"fixtures/orders\",\"version\":\"1\"}},\"version\":\"1\"},{\"orders\":{\"tools\":[{\"inputSchema\":{\"additionalProperties\":{\"type\":\"integer\"},\"type\":\"object\"},\"name\":\"counts\"},{\"inputSchema\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"arguments\":{\"type\":\"string\"},\"class\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"limit\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"},\"options\":{\"type\":\"string\"},\"order-id\":{\"type\":\"string\"},\"self\":{\"type\":\"string\"},\"timeout\":{\"type\":\"string\"}},\"required\":[\"id\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"arguments\":{\"type\":\"string\"},\"class\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"limit\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"},\"options\":{\"type\":\"string\"},\"order-id\":{\"type\":\"string\"},\"self\":{\"type\":\"string\"},\"timeout\":{\"type\":\"string\"}},\"required\":[\"id\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"email\":{\"type\":\"string\"}},\"required\":[\"email\"],\"type\":\"object\"}],\"type\":\"object\"},\"name\":\"find\"},{\"inputSchema\":{\"additionalProperties\":false,\"properties\":{\"arguments\":{\"type\":\"string\"},\"class\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"limit\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"},\"options\":{\"type\":\"string\"},\"order-id\":{\"type\":\"string\"},\"self\":{\"type\":\"string\"},\"timeout\":{\"type\":\"string\"}},\"required\":[\"id\"],\"type\":\"object\"},\"name\":\"get-order\",\"outputSchema\":{\"properties\":{\"total\":{\"type\":\"number\"}},\"required\":[\"total\"],\"type\":\"object\"}},{\"inputSchema\":{\"additionalProperties\":false,\"type\":\"object\"},\"name\":\"ping\"},{\"inputSchema\":{\"additionalProperties\":false,\"properties\":{\"arguments\":{\"type\":\"string\"},\"class\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"limit\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"},\"options\":{\"type\":\"string\"},\"order-id\":{\"type\":\"string\"},\"self\":{\"type\":\"string\"},\"timeout\":{\"type\":\"string\"}},\"required\":[\"id\"],\"type\":\"object\"},\"name\":\"read\",\"outputSchema\":{\"properties\":{\"total\":{\"type\":\"number\"}},\"required\":[\"total\"],\"type\":\"object\"}}]}}]")
102
+
103
+ apps = _typing.cast(_Clients, _Apps(_identity, _contracts))
104
+
105
+ def create_apps(bindings: _typing.Mapping[str, _Binding]) -> _Clients:
106
+ return _typing.cast(_Clients, _Apps(_identity, _contracts, bindings))
@@ -0,0 +1,57 @@
1
+ """Static tool catalog for generated-client typing regressions."""
2
+
3
+ from mcp.server import Server
4
+ from mcp.types import Tool
5
+
6
+ server = Server("orders")
7
+
8
+ READ_INPUT = {
9
+ "type": "object",
10
+ "properties": {
11
+ "id": {"type": "string"},
12
+ "limit": {"type": "integer"},
13
+ "name": {"type": "string"},
14
+ "timeout": {"type": "string"},
15
+ "options": {"type": "string"},
16
+ "self": {"type": "string"},
17
+ "arguments": {"type": "string"},
18
+ "class": {"type": "string"},
19
+ "order-id": {"type": "string"},
20
+ },
21
+ "required": ["id"],
22
+ "additionalProperties": False,
23
+ }
24
+ READ_OUTPUT = {
25
+ "type": "object",
26
+ "properties": {"total": {"type": "number"}},
27
+ "required": ["total"],
28
+ }
29
+
30
+
31
+ @server.list_tools()
32
+ async def tools() -> list[Tool]:
33
+ return [
34
+ Tool(name="read", input_schema=READ_INPUT, output_schema=READ_OUTPUT),
35
+ Tool(name="get-order", input_schema=READ_INPUT, output_schema=READ_OUTPUT),
36
+ Tool(name="ping", input_schema={"type": "object", "additionalProperties": False}),
37
+ Tool(
38
+ name="counts",
39
+ input_schema={"type": "object", "additionalProperties": {"type": "integer"}},
40
+ ),
41
+ Tool(
42
+ name="find",
43
+ input_schema={
44
+ "type": "object",
45
+ "anyOf": [
46
+ READ_INPUT,
47
+ READ_INPUT,
48
+ {
49
+ "type": "object",
50
+ "properties": {"email": {"type": "string"}},
51
+ "required": ["email"],
52
+ "additionalProperties": False,
53
+ },
54
+ ],
55
+ },
56
+ ),
57
+ ]
@@ -0,0 +1,67 @@
1
+ """Consumer contracts at the generated namespace boundary."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def test_generated_namespace_combines_app_types_with_installed_runtime(tmp_path):
11
+ # Refresh with scripts/fixtures.sh; generation is tested in holon-sh/cli.
12
+ shutil.copytree(
13
+ Path(__file__).parent / "fixtures/caller", tmp_path, dirs_exist_ok=True
14
+ )
15
+ (tmp_path / "check.py").write_text("""from pathlib import Path
16
+ import holon
17
+ import holon.client
18
+ from holon.apps import apps, Inputs, Outputs
19
+
20
+ async def run() -> float:
21
+ arguments: Inputs.orders.read = {
22
+ "id": "order-1", "limit": 2, "name": "n", "timeout": "t", "options": "o",
23
+ "self": "s", "arguments": "a", "class": "c", "order-id": "id",
24
+ }
25
+ output: Outputs.orders.read = await apps.orders.tools.read(**arguments)
26
+ await apps.orders.call_tool("read", {"timeout": 5}, **arguments)
27
+ await apps.orders.call_tool("get-order", id="order-1")
28
+ await apps.orders.tools.ping()
29
+ await apps.orders.tools.counts(apples=2, oranges=3)
30
+ await apps.orders.tools.find(id="order-1")
31
+ await apps.orders.call_tool("find", email="user@example.com")
32
+ return output["total"]
33
+
34
+ async def invalid() -> None:
35
+ await apps.orders.tools.read(id=1) # type: ignore
36
+ await apps.orders.tools.read() # type: ignore
37
+ await apps.orders.call_tool("read", id=1) # type: ignore
38
+ await apps.orders.call_tool("read") # type: ignore
39
+ await apps.orders.tools.read({"id": "old form"}) # type: ignore
40
+ await apps.orders.tools.ping(unexpected=True) # type: ignore
41
+ await apps.orders.tools.counts(apples="two") # type: ignore
42
+ await apps.orders.tools.find() # type: ignore
43
+ await apps.orders.call_tool("find", id=1) # type: ignore
44
+ await apps.orders.tools.find(id="x", email="user@example.com") # type: ignore
45
+ apps.missing # type: ignore
46
+ apps.orders.missing
47
+
48
+ assert holon.__file__ is None
49
+ assert Path(holon.client.__file__).parent != Path(__file__).parent / "holon"
50
+ """)
51
+ # Runtime and typing must agree across the two namespace portions. No
52
+ # MYPYPATH/PYTHONPATH override is allowed to hide packaging problems.
53
+ env = os.environ.copy()
54
+ env.pop("MYPYPATH", None)
55
+ env.pop("PYTHONPATH", None)
56
+ result = subprocess.run(
57
+ [sys.executable, "-m", "mypy", "--strict", "--no-error-summary", "check.py"],
58
+ cwd=tmp_path,
59
+ env=env,
60
+ capture_output=True,
61
+ text=True,
62
+ check=False,
63
+ )
64
+ assert result.returncode == 1
65
+ assert len(result.stdout.splitlines()) == 1, result.stdout
66
+ assert '"OrdersApp" has no attribute "missing"' in result.stdout
67
+ subprocess.run([sys.executable, "check.py"], cwd=tmp_path, env=env, check=True)
@@ -19,6 +19,8 @@ CONTRACT = {
19
19
  "properties": {
20
20
  "id": {"type": "string"},
21
21
  "limit": {"type": "integer", "default": 5},
22
+ "name": {"type": "string"},
23
+ "timeout": {"type": "integer"},
22
24
  },
23
25
  "required": ["id"],
24
26
  "additionalProperties": False,
@@ -40,7 +42,9 @@ def test_clients_preserve_mcp_results_validate_and_cancel(monkeypatch):
40
42
  server = MCPServer("orders")
41
43
 
42
44
  @server.tool()
43
- async def read(id: str, limit: int = 5) -> CallToolResult | InputRequiredResult:
45
+ async def read(
46
+ id: str, limit: int = 5, name: str = "", timeout: int = 0
47
+ ) -> CallToolResult | InputRequiredResult:
44
48
  if id == "error":
45
49
  return CallToolResult(
46
50
  is_error=True,
@@ -96,16 +100,37 @@ def test_clients_preserve_mcp_results_validate_and_cancel(monkeypatch):
96
100
  other = Apps({**identity, "name": "other"}, {"orders": CONTRACT})
97
101
  assert not requests
98
102
  async with app.router.lifespan_context(app):
99
- arguments = {"id": "good"}
100
103
  output, raw = await asyncio.gather(
101
- apps.orders.tools.read(arguments),
102
- other.orders.call_tool("read", {"id": "good", "limit": 2}),
104
+ apps.orders.tools.read(id="good"),
105
+ other.orders.call_tool(
106
+ "read",
107
+ {
108
+ "timeout": 5,
109
+ "meta": {"trace": "control"},
110
+ },
111
+ id="good",
112
+ limit=2,
113
+ name="field",
114
+ timeout=7,
115
+ ),
103
116
  )
104
117
  assert output == {"total": 5}
105
- assert arguments == {"id": "good"}
106
118
  assert raw.structured_content == {"total": 2}
107
119
  assert raw.content[0].text == "Order"
108
120
  assert raw.meta["preserved"] is True
121
+ request = next(
122
+ request["params"]
123
+ for request in requests
124
+ if request["method"] == "tools/call"
125
+ and request["params"]["arguments"].get("name") == "field"
126
+ )
127
+ assert request["arguments"] == {
128
+ "id": "good",
129
+ "limit": 2,
130
+ "name": "field",
131
+ "timeout": 7,
132
+ }
133
+ assert request["_meta"]["trace"] == "control"
109
134
  assert not any(request["method"] == "tools/list" for request in requests)
110
135
  callers = [
111
136
  request["params"]["_meta"]["io.modelcontextprotocol/clientInfo"]["name"]
@@ -114,9 +139,9 @@ def test_clients_preserve_mcp_results_validate_and_cancel(monkeypatch):
114
139
  ]
115
140
  assert set(callers) == {"checkout", "other"}
116
141
  for value, code in [("error", "tool_error"), ("input", "input_required")]:
117
- raw = await apps.orders.call_tool("read", {"id": value})
142
+ raw = await apps.orders.call_tool("read", id=value)
118
143
  with pytest.raises(ToolCallError) as error:
119
- await apps.orders.tools.read({"id": value})
144
+ await apps.orders.tools.read(id=value)
120
145
  assert error.value.code == code
121
146
  assert error.value.app == "orders"
122
147
  assert error.value.tool == "read"
@@ -124,26 +149,33 @@ def test_clients_preserve_mcp_results_validate_and_cancel(monkeypatch):
124
149
  assert error.value.result.meta["preserved"] is True
125
150
  if isinstance(raw, InputRequiredResult):
126
151
  assert error.value.result.request_state
152
+ await apps.orders.call_tool(
153
+ "read",
154
+ {"request_state": raw.request_state, "input_responses": {}},
155
+ id=value,
156
+ )
157
+ assert requests[-1]["params"]["requestState"] == raw.request_state
158
+ assert requests[-1]["params"]["inputResponses"] == {}
127
159
  else:
128
160
  assert error.value.result == raw
129
161
  before = len(requests)
130
162
  with pytest.raises(ValueError, match="arguments"):
131
- await apps.orders.tools.read({"id": 1})
163
+ await apps.orders.tools.read(id=1)
132
164
  assert len(requests) == before
133
165
  with pytest.raises(ValueError, match="structured output"):
134
- await apps.orders.tools.read({"id": "bad"})
135
- task = asyncio.create_task(apps.orders.tools.read({"id": "slow"}))
166
+ await apps.orders.tools.read(id="bad")
167
+ task = asyncio.create_task(apps.orders.tools.read(id="slow"))
136
168
  await started.wait()
137
169
  task.cancel()
138
170
  with pytest.raises(asyncio.CancelledError):
139
171
  await task
140
172
  await cancelled.wait()
141
173
  untyped = Apps({**identity, "requires": {"orders": {}, "missing": {}}}, {})
142
- raw = await untyped.orders.call_tool("read", {"id": "good"})
174
+ raw = await untyped.orders.call_tool("read", id="good")
143
175
  assert raw.structured_content == {"total": 5}
144
176
  assert not hasattr(untyped.orders.tools, "read")
145
177
  with pytest.raises(RuntimeError, match=r"unavailable \(MISSING_MCP_URL\)"):
146
- await untyped.missing.call_tool("read", {})
178
+ await untyped.missing.call_tool("read")
147
179
  # Explicit bindings work without a declaration and override the resolver.
148
180
  set_default_binding(
149
181
  lambda _: {"endpoint": "http://unavailable.invalid/mcp"}
@@ -156,12 +188,10 @@ def test_clients_preserve_mcp_results_validate_and_cancel(monkeypatch):
156
188
  with pytest.raises(ValueError, match="Unknown app binding"):
157
189
  Apps(script, {"orders": CONTRACT}, {"missing": binding})
158
190
  bound = Apps(script, {"orders": CONTRACT}, {"orders": binding})
159
- assert await bound.orders.tools.read({"id": "good"}) == {"total": 5}
191
+ assert await bound.orders.tools.read(id="good") == {"total": 5}
160
192
  set_default_binding(lambda _: None)
161
193
  with pytest.raises(ValueError, match="not declared"):
162
- await Apps(script, {"orders": CONTRACT}).orders.tools.read(
163
- {"id": "good"}
164
- )
194
+ await Apps(script, {"orders": CONTRACT}).orders.tools.read(id="good")
165
195
  assert headers and all(value == "Bearer external" for value in headers)
166
196
  assert all(client.is_closed for client in clients)
167
197
 
@@ -1,140 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: holon-sh
3
- Version: 0.0.1.dev2
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-sh
15
-
16
- The Python runtime for typed MCP clients. The [`holon` CLI](https://github.com/holon-sh/cli) reads the MCP servers your server calls and generates `holon.apps`, a typed client module, into your project. This package is what that module runs on: it connects over Streamable HTTP, validates arguments and results against each tool's JSON Schema, and raises on tool errors.
17
-
18
- Start with the [CLI README](https://github.com/holon-sh/cli) for the walkthrough. This page is the Python API.
19
-
20
- ## Install
21
-
22
- ```sh
23
- uv add --prerelease allow holon-sh
24
- uv add --dev mypy==2.4.0 # holon types check runs mypy from the app's .venv
25
- ```
26
-
27
- The distribution is `holon-sh`; the import package is `holon`, a namespace package shared with your generated module. Do not create `holon/__init__.py` in your app, and do not edit `holon/apps/__init__.py`: the CLI rewrites it on every run.
28
-
29
- ## The generated module
30
-
31
- ```python
32
- from holon.apps import Inputs, Outputs, apps, create_apps
33
- ```
34
-
35
- | Name | What it is |
36
- |---|---|
37
- | `apps` | One client per dependency in `server.json` (`apps.orders`), bound through `<NAME>_MCP_URL`. |
38
- | `create_apps(bindings)` | Clients bound explicitly, for only the dependencies you pass. |
39
- | `Inputs.<dep>.<tool>` | The tool's argument type, a `TypedDict`. |
40
- | `Outputs.<dep>.<tool>` | The tool's structured result type, a `TypedDict`. |
41
-
42
- ### Call a tool
43
-
44
- ```python
45
- from holon.apps import apps
46
-
47
- async def total(id: str) -> float:
48
- order = await apps.orders.tools.lookup({"id": id}, timeout=5.0)
49
- return order["total"]
50
- ```
51
-
52
- Arguments are one mapping, validated before sending; the result is validated against the output schema and returned. `timeout` is in seconds.
53
-
54
- ### Handle errors
55
-
56
- `tools.<tool>` raises `ToolCallError` when the tool reports an error or asks for more input:
57
-
58
- ```python
59
- from holon.client import ToolCallError
60
-
61
- try:
62
- order = await apps.orders.tools.lookup({"id": id})
63
- except ToolCallError as error:
64
- error.code # "tool_error" or "input_required"
65
- error.result # the original MCP result
66
- ```
67
-
68
- Invalid arguments or a result that does not match the schema raise `ValueError`; transport failures propagate from the MCP client.
69
-
70
- ### Read the whole result
71
-
72
- `call_tool` returns the full MCP result instead of raising:
73
-
74
- ```python
75
- result = await apps.orders.call_tool("lookup", {"id": id})
76
- if result.is_error:
77
- ... # inspect result.content
78
- else:
79
- result.structured_content # typed as Outputs.orders.lookup
80
- ```
81
-
82
- It also accepts `input_responses`, `request_state` and `meta`. A dependency the CLI could not obtain a contract for offers only `call_tool`, untyped.
83
-
84
- ## Bind a dependency
85
-
86
- By default a client reads its endpoint from an environment variable named after the dependency, upper-cased, with `-` as `_`:
87
-
88
- ```sh
89
- ORDERS_MCP_URL=http://127.0.0.1:3001/mcp uv run python server.py
90
- ```
91
-
92
- For authentication or a custom HTTP client, bind explicitly. `http_client` is a factory called once per call; the client it returns is closed afterwards:
93
-
94
- ```python
95
- import os
96
- import httpx2
97
- from holon.apps import create_apps
98
-
99
- apps = create_apps({
100
- "orders": {
101
- "endpoint": "https://orders.example.com/mcp",
102
- "http_client": lambda: httpx2.AsyncClient(
103
- headers={"Authorization": f"Bearer {os.environ['ORDERS_TOKEN']}"},
104
- ),
105
- },
106
- })
107
- ```
108
-
109
- Every call opens and closes its own session; there is nothing to close. `holon.binding.set_default_binding` exists for hosts that bind every app at startup; apps do not need it.
110
-
111
- ## Serve an MCP App view
112
-
113
- `holon.view.app_view` registers a built HTML file as an MCP App resource:
114
-
115
- ```python
116
- from holon.view import app_view
117
-
118
- server.add_resource(app_view(
119
- "ui://checkout/index.html", "Checkout", "dist/index.html",
120
- connect_domains=["https://orders.example.com"],
121
- ))
122
- ```
123
-
124
- The file is read on first request and cached. `connect_domains` and `resource_domains` become the view's content security policy. The page itself uses [`holon-ui`](https://github.com/holon-sh/sdk-ui).
125
-
126
- ## Development
127
-
128
- The package test uses committed clients in `tests/fixtures/caller/holon/apps`.
129
- To refresh them, install the [holon CLI](https://github.com/holon-sh/cli) and run:
130
-
131
- ```sh
132
- ./scripts/fixtures.sh
133
- ```
134
-
135
- This runs `holon types generate ../orders` from the caller fixture. The CLI
136
- extracts `tests/fixtures/orders/server.py` without starting the server.
137
-
138
- ## License
139
-
140
- MIT
@@ -1,127 +0,0 @@
1
- # holon-sh
2
-
3
- The Python runtime for typed MCP clients. The [`holon` CLI](https://github.com/holon-sh/cli) reads the MCP servers your server calls and generates `holon.apps`, a typed client module, into your project. This package is what that module runs on: it connects over Streamable HTTP, validates arguments and results against each tool's JSON Schema, and raises on tool errors.
4
-
5
- Start with the [CLI README](https://github.com/holon-sh/cli) for the walkthrough. This page is the Python API.
6
-
7
- ## Install
8
-
9
- ```sh
10
- uv add --prerelease allow holon-sh
11
- uv add --dev mypy==2.4.0 # holon types check runs mypy from the app's .venv
12
- ```
13
-
14
- The distribution is `holon-sh`; the import package is `holon`, a namespace package shared with your generated module. Do not create `holon/__init__.py` in your app, and do not edit `holon/apps/__init__.py`: the CLI rewrites it on every run.
15
-
16
- ## The generated module
17
-
18
- ```python
19
- from holon.apps import Inputs, Outputs, apps, create_apps
20
- ```
21
-
22
- | Name | What it is |
23
- |---|---|
24
- | `apps` | One client per dependency in `server.json` (`apps.orders`), bound through `<NAME>_MCP_URL`. |
25
- | `create_apps(bindings)` | Clients bound explicitly, for only the dependencies you pass. |
26
- | `Inputs.<dep>.<tool>` | The tool's argument type, a `TypedDict`. |
27
- | `Outputs.<dep>.<tool>` | The tool's structured result type, a `TypedDict`. |
28
-
29
- ### Call a tool
30
-
31
- ```python
32
- from holon.apps import apps
33
-
34
- async def total(id: str) -> float:
35
- order = await apps.orders.tools.lookup({"id": id}, timeout=5.0)
36
- return order["total"]
37
- ```
38
-
39
- Arguments are one mapping, validated before sending; the result is validated against the output schema and returned. `timeout` is in seconds.
40
-
41
- ### Handle errors
42
-
43
- `tools.<tool>` raises `ToolCallError` when the tool reports an error or asks for more input:
44
-
45
- ```python
46
- from holon.client import ToolCallError
47
-
48
- try:
49
- order = await apps.orders.tools.lookup({"id": id})
50
- except ToolCallError as error:
51
- error.code # "tool_error" or "input_required"
52
- error.result # the original MCP result
53
- ```
54
-
55
- Invalid arguments or a result that does not match the schema raise `ValueError`; transport failures propagate from the MCP client.
56
-
57
- ### Read the whole result
58
-
59
- `call_tool` returns the full MCP result instead of raising:
60
-
61
- ```python
62
- result = await apps.orders.call_tool("lookup", {"id": id})
63
- if result.is_error:
64
- ... # inspect result.content
65
- else:
66
- result.structured_content # typed as Outputs.orders.lookup
67
- ```
68
-
69
- It also accepts `input_responses`, `request_state` and `meta`. A dependency the CLI could not obtain a contract for offers only `call_tool`, untyped.
70
-
71
- ## Bind a dependency
72
-
73
- By default a client reads its endpoint from an environment variable named after the dependency, upper-cased, with `-` as `_`:
74
-
75
- ```sh
76
- ORDERS_MCP_URL=http://127.0.0.1:3001/mcp uv run python server.py
77
- ```
78
-
79
- For authentication or a custom HTTP client, bind explicitly. `http_client` is a factory called once per call; the client it returns is closed afterwards:
80
-
81
- ```python
82
- import os
83
- import httpx2
84
- from holon.apps import create_apps
85
-
86
- apps = create_apps({
87
- "orders": {
88
- "endpoint": "https://orders.example.com/mcp",
89
- "http_client": lambda: httpx2.AsyncClient(
90
- headers={"Authorization": f"Bearer {os.environ['ORDERS_TOKEN']}"},
91
- ),
92
- },
93
- })
94
- ```
95
-
96
- Every call opens and closes its own session; there is nothing to close. `holon.binding.set_default_binding` exists for hosts that bind every app at startup; apps do not need it.
97
-
98
- ## Serve an MCP App view
99
-
100
- `holon.view.app_view` registers a built HTML file as an MCP App resource:
101
-
102
- ```python
103
- from holon.view import app_view
104
-
105
- server.add_resource(app_view(
106
- "ui://checkout/index.html", "Checkout", "dist/index.html",
107
- connect_domains=["https://orders.example.com"],
108
- ))
109
- ```
110
-
111
- The file is read on first request and cached. `connect_domains` and `resource_domains` become the view's content security policy. The page itself uses [`holon-ui`](https://github.com/holon-sh/sdk-ui).
112
-
113
- ## Development
114
-
115
- The package test uses committed clients in `tests/fixtures/caller/holon/apps`.
116
- To refresh them, install the [holon CLI](https://github.com/holon-sh/cli) and run:
117
-
118
- ```sh
119
- ./scripts/fixtures.sh
120
- ```
121
-
122
- This runs `holon types generate ../orders` from the caller fixture. The CLI
123
- extracts `tests/fixtures/orders/server.py` without starting the server.
124
-
125
- ## License
126
-
127
- MIT
@@ -1,55 +0,0 @@
1
- # holon-generated-apps-v1 app=caller
2
- from __future__ import annotations
3
-
4
- import json as _json
5
-
6
- from mcp.types import InputResponses as _InputResponses
7
- from mcp.types import RequestParamsMeta as _RequestParamsMeta
8
-
9
- from holon.client import Apps as _Apps
10
- from holon.client import Binding as _Binding
11
- from holon.client import ToolResult as _ToolResult
12
-
13
- import builtins as _builtins
14
- import typing as _typing
15
-
16
- _Shape0 = _typing.TypedDict("_Shape0", {"id": _builtins.str})
17
-
18
- _Shape1 = _typing.TypedDict("_Shape1", {"total": _builtins.float})
19
-
20
- class Inputs:
21
- class orders:
22
- read: _typing.TypeAlias = _Shape0
23
-
24
- class Outputs:
25
- class orders:
26
- read: _typing.TypeAlias = _Shape1
27
-
28
- class InputTypes:
29
- class orders:
30
- class read:
31
- pass
32
-
33
- class OutputTypes:
34
- class orders:
35
- class read:
36
- pass
37
-
38
- class _Tools0(_typing.Protocol):
39
- async def read(self, arguments: _Shape0, *, timeout: _builtins.float | None = ...) -> _Shape1: ...
40
-
41
- class _App0(_typing.Protocol):
42
- tools: _Tools0
43
- async def call_tool(self, name: _typing.Literal["read"], arguments: _Shape0, *, timeout: _builtins.float | None = ..., input_responses: _InputResponses | None = ..., request_state: _builtins.str | None = ..., meta: _RequestParamsMeta | None = ...) -> _ToolResult[_Shape1]: ...
44
-
45
- class _Clients(_typing.Protocol):
46
- orders: _App0
47
- def __getitem__(self, name: _typing.Literal["orders"]) -> _App0: ...
48
-
49
-
50
- _identity, _contracts = _json.loads('[{"name":"fixtures/caller","requires":{"orders":{"name":"fixtures/orders","version":"1"}},"version":"1"},{"orders":{"tools":[{"name":"read","inputSchema":{"properties":{"id":{"type":"string"}},"required":["id"],"type":"object"},"outputSchema":{"properties":{"total":{"type":"number"}},"required":["total"],"type":"object"}}]}}]')
51
-
52
- apps = _typing.cast(_Clients, _Apps(_identity, _contracts))
53
-
54
- def create_apps(bindings: _typing.Mapping[str, _Binding]) -> _Clients:
55
- return _typing.cast(_Clients, _Apps(_identity, _contracts, bindings))
@@ -1,14 +0,0 @@
1
- from typing import TypedDict
2
-
3
- from mcp.server import MCPServer
4
-
5
- server = MCPServer("orders")
6
-
7
-
8
- class Order(TypedDict):
9
- total: float
10
-
11
-
12
- @server.tool()
13
- def read(id: str) -> Order:
14
- return {"total": 42.0}
@@ -1,40 +0,0 @@
1
- """Consumer contracts at the generated namespace boundary."""
2
-
3
- import os
4
- import shutil
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
-
9
-
10
- def test_generated_namespace_combines_app_types_with_installed_runtime(tmp_path):
11
- # Refresh with scripts/fixtures.sh; generation is tested in holon-sh/cli.
12
- shutil.copytree(Path(__file__).parent / "fixtures/caller", tmp_path, dirs_exist_ok=True)
13
- (tmp_path / "check.py").write_text("""from pathlib import Path
14
- import holon
15
- import holon.client
16
- from holon.apps import apps, Inputs, Outputs
17
-
18
- async def run() -> float:
19
- arguments: Inputs.orders.read = {"id": "order-1"}
20
- output: Outputs.orders.read = await apps.orders.tools.read(arguments)
21
- return output["total"]
22
-
23
- async def invalid() -> None:
24
- await apps.orders.tools.read({"id": 1}) # type: ignore
25
- await apps.orders.tools.read({}) # type: ignore
26
- apps.missing # type: ignore
27
-
28
- assert holon.__file__ is None
29
- assert Path(holon.client.__file__).parent != Path(__file__).parent / "holon"
30
- """)
31
- # Runtime and typing must agree across the two namespace portions. No
32
- # MYPYPATH/PYTHONPATH override is allowed to hide packaging problems.
33
- env = os.environ.copy()
34
- env.pop("MYPYPATH", None)
35
- env.pop("PYTHONPATH", None)
36
- for command in [
37
- [sys.executable, "-m", "mypy", "--strict", "--no-incremental", "check.py"],
38
- [sys.executable, "check.py"],
39
- ]:
40
- subprocess.run(command, cwd=tmp_path, env=env, check=True)
File without changes
File without changes