Metadata-Version: 2.5
Name: holon-sh
Version: 0.0.1.dev6
Summary: Typed MCP clients and the caller of an MCP request
Project-URL: Repository, https://github.com/holon-sh/sdk-py
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: httpx2<3,>=2.10
Requires-Dist: jsonschema<5,>=4.26
Requires-Dist: mcp<3,>=2.3
Description-Content-Type: text/markdown

# Holon Python SDK

[![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)
[![PyPI](https://img.shields.io/pypi/v/holon-sh)](https://pypi.org/project/holon-sh/)
[![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
[![Apache 2.0 License](https://img.shields.io/badge/license-Apache--2.0-green)](LICENSE)

This repository contains the Python runtime for [`holon`](https://github.com/holon-sh/cli).

## Install

```sh
uv add --prerelease allow holon-sh
uv add --dev holon-types
```

`holon-types` is the `holon` CLI; it brings mypy, which `holon types check` runs from the project's environment.

## Invoking MCP Servers

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.

### Type-checking MCP invocations

```sh
uv run holon types check
```

<details open>
<summary><h3>Call a tool</h3></summary>

```python
from holon.apps import apps


async def greet(name: str) -> str:
    greeting = await apps.bar.tools.greet({"timeout": 5.0}, name=name)
    return greeting["result"]
```

Arguments are keywords; the result is validated against the output schema and returned.

</details>

<details open>
<summary><h3>Handle errors</h3></summary>

```python
from holon.client import ToolCallError

try:
    await apps.bar.tools.greet(name="world")
except ToolCallError as error:
    error.result  # the original MCP result
```

</details>

<details open>
<summary><h3>Read the whole result</h3></summary>

`call_tool` returns the full MCP result instead of raising:

```python
result = await apps.bar.call_tool("greet", name="world")
if result.result_type == "input_required":
    ...  # call again with input_responses and request_state
elif result.is_error:
    ...  # inspect result.content
else:
    result.structured_content  # typed as Outputs.bar.greet
```

The options dict also accepts `input_responses`, `request_state` and `meta`.

</details>

<details open>
<summary><h3>Answer a server's question</h3></summary>

A tool can stop and ask for input through an [MCP elicitation](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation). Pass `elicit` to answer in place, and the call returns its usual typed output:

```python
from mcp.types import ElicitResult


async def elicit(params):
    return ElicitResult(action="accept", content=await ask(params.message))


greeting = await apps.bar.tools.greet({"elicit": elicit}, name="world")
```

Without `elicit`, the question arrives on `ToolCallError.result`, or as an `input_required` result from `call_tool`. To answer later, for example in another request, call again with `input_responses` and `request_state`.

</details>

## Identify the caller

Inside a tool handler, `get_context().principal` is the user Holon admitted for the request, or `None` for anonymous requests:

```python
from holon.context import get_context

principal = get_context().principal
if principal is not None:
    principal.issuer, principal.subject, principal.scopes
    principal.claims.get("email")  # standard OpenID claims are typed: str | None
```
