API reference

August 2, 2026 ยท View on GitHub

This page describes the complete supported API in the 2.0.0a5 alpha. Imports not exposed from samsarix_core.__all__ are internal.

samsarix_tool

@samsarix_tool(
    name: str | None = None,
    description: str | None = None,
    timeout: float | None = None,
    version: str = "1",
    tags: tuple[str, ...] = (),
    title: str | None = None,
    read_only: bool = False,
    destructive: bool | None = None,
    idempotent: bool | None = None,
    open_world: bool = True,
    task_support: TaskSupport = "forbidden",
)

Works as @samsarix_tool or @samsarix_tool(...) on standalone sync and async functions. Names use [A-Za-z][A-Za-z0-9_-]{0,63}. Descriptions fall back to the first docstring line. Unsupported types, unresolved annotations, generators, positional- only parameters, *args, and **kwargs raise ToolDefinitionError. The title and four behavioral options produce MCP display and behavioral annotations. Read-only tools infer destructive=False and idempotent=True; other tools retain MCP's conservative destructive/non-idempotent defaults unless explicitly annotated. task_support accepts "forbidden", "optional", or "required" and controls experimental MCP task augmentation only when the server and 2025-11-25 client negotiate that capability. It does not alter direct runtime invocation.

ToolRegistry

ToolRegistry(*, max_tools: int = 256)
  • register(function, *, replace=False) -> ToolSpec
  • unregister(name) -> ToolSpec
  • get(name) -> ToolSpec
  • list() -> tuple[ToolSpec, ...]
  • schema_catalog() -> dict

Duplicate registration raises DuplicateToolError unless replacement is explicit. Adding a new tool at capacity raises RegistryCapacityError; replacing an existing tool remains allowed. Unknown direct lookups/removals raise ToolNotFoundError. Runtime lookup failures become structured results instead.

ToolRuntime

ToolRuntime(
    registry: ToolRegistry | None = None,
    *,
    max_concurrency: int = 8,
    max_pending_invocations: int = 256,
    max_batch_size: int = 256,
    max_argument_bytes: int = 1_048_576,
    max_output_bytes: int = 1_048_576,
    max_value_depth: int = 32,
    max_value_nodes: int = 10_000,
    max_progress_updates: int = 1_000,
    max_progress_message_bytes: int = 4_096,
    default_timeout: float = 30.0,
    expose_exceptions: bool = False,
    policy: ToolPolicy | None = None,
    lifecycle_handler: ToolLifecycleHandler | None = None,
)
  • register(function, *, replace=False, max_concurrency=None) -> ToolSpec
  • await invoke(name, arguments=None, *, timeout=None, progress_handler=None) -> ToolResult
  • await invoke_many(calls) -> list[ToolResult]
  • metrics() -> RuntimeMetrics
  • pending_sync_calls -> int
  • await wait_for_sync(*, timeout=None) -> bool
  • await aclose(*, wait_for_sync=False, timeout=None) -> bool

Timeout precedence is invocation override, decorator timeout, then runtime default. The timeout includes policy evaluation and time waiting for a concurrency slot. Batch results preserve order. A batch larger than max_batch_size raises ValueError before any call is started. aclose() is idempotent.

max_pending_invocations bounds all admitted calls that have not reached a terminal result, including validation, policy, execution-slot waiting, and execution. Calls beyond the cap fail fast with status busy, safe code runtime_busy, and retryable=True; their arguments are not retained in runtime metrics or errors. RuntimeMetrics.busy, pending_invocations, and peak_pending_invocations expose content-free saturation signals. Batch workers are capped by pending capacity and execution remains bounded by the global and per-tool semaphores. This lets an unrelated call later in a mixed batch reach free execution capacity when it fits within available pending capacity, instead of waiting behind workers queued on one constrained tool. An isolated batch still processes every accepted item rather than self-shedding. This process-local limit is not a request-rate or per-tenant quota.

register(..., max_concurrency=N) gives that exact tool registration its own positive execution limit. Calls acquire the per-tool semaphore before the runtime-wide semaphore, so waiters for one constrained or unhealthy dependency do not occupy global execution slots needed by unrelated tools. The limit applies identically to direct, batch, MCP, and task-augmented MCP invocation, but is host deployment policy and is therefore absent from ToolSpec and MCP discovery. Its wait time is covered by the invocation timeout and its waiters are covered by max_pending_invocations. Replacing a tool without supplying a limit makes the replacement unbounded apart from the runtime-wide cap. Booleans and non-integers raise TypeError; non-positive integers raise ValueError before the tool is registered.

Argument and output sizes are the UTF-8 byte length of compact JSON. The root is depth zero; every child increments depth. Each container and scalar is one node, while object keys are covered by the byte limit but do not count as nodes. Cyclic, oversized, over-depth, and over-node arguments return invalid_arguments without executing the tool. Output resource violations return failed with the safe error code output_limit_exceeded. All integer limits must be positive and reject booleans.

A timed-out or caller-cancelled sync callable cannot be killed safely. It remains in pending_sync_calls, keeps its global and per-tool semaphore slots, and remains included in metrics().in_flight until the underlying thread actually stops. This prevents timeouts from feeding an unbounded executor queue. Late exceptions are consumed without being exposed through the event loop.

wait_for_sync() snapshots currently submitted sync calls and returns False if its optional non-negative timeout expires. For a race-free shutdown sequence, stop external admission or close the runtime first. aclose() rejects new calls, cancels active async waits, cancels sync work that has not started, and returns whether submitted sync work is quiescent. Its default does not wait; pass wait_for_sync=True with a finite timeout when shutdown needs bounded quiescence. Calling it again is safe. The async context manager uses the non-waiting default.

lifecycle_handler is an optional synchronous callable receiving an immutable, content-free ToolLifecycleEvent at logical invocation start and termination. Normal terminal values mirror ToolStatus; caller cancellation emits cancelled, while an exception crossing the host boundary emits aborted. Events include only invocation ID, requested tool name, status, UTC occurrence time, and terminal duration. Handler exceptions and accidental awaitable returns are isolated and counted by RuntimeMetrics.lifecycle_handler_failures; declared async handlers are rejected. Delivery is inline, so handlers must remain non-blocking and hand off network export to a bounded application-owned processor. See Lifecycle observability.

policy must be an async callable that receives one ToolPolicyContext after the tool exists and its arguments pass schema/resource validation. The context contains the invocation ID, a detached ToolSpec, and a detached default-filled argument mapping. Return ToolPolicyDecision.ALLOW or ToolPolicyDecision.DENY. Policy evaluation is bounded to max_concurrency; denial returns status denied with safe code tool_denied, while exceptions or any other return value fail closed with status failed and safe code tool_policy_failed. Neither path runs the tool. The same policy applies to direct, batch, ordinary MCP, and task-augmented MCP calls. Policy context is not serialized or logged by Core, but it contains application input and must be treated as sensitive. Use an outer persistent workflow for long-running human approval.

progress_handler is an optional sync or async callable receiving immutable ToolProgress values. An asynchronous tool reports through await report_progress(progress, total=None, message=None). Updates must use finite non-negative numbers and strictly increase. The helper returns False when no handler is active, the invocation is complete, or the configured update cap is exhausted. Progress messages are UTF-8 bounded separately and may contain sensitive application text, so tools should keep them generic. Synchronous tools do not have an async reporting context. A non-increasing progress value or an oversized message raises ValueError and fails the tool invocation.

MCPServer

MCPServer(
    runtime: ToolRuntime,
    *,
    name: str = "samsarix-core",
    title: str = "Samsarix Core",
    version: str = __version__,
    instructions: str | None = None,
    enable_logging: bool = False,
    default_log_level: str = "warning",
    enable_tasks: bool = False,
    max_retained_tasks: int = 64,
    default_task_ttl_ms: int = 300_000,
    max_task_ttl_ms: int = 3_600_000,
    task_poll_interval_ms: int = 500,
)
  • await handle(message, *, notification_sender=None) -> dict | None
  • await aclose(*, close_runtime=True) -> None

handle() accepts one parsed MCP JSON-RPC message. It supports lifecycle initialization, ping, tools/list, tools/call, initialized notifications, and notifications/cancelled for active calls. With logging enabled, it advertises the MCP logging capability, accepts logging/setLevel, and can emit one content-free terminal notifications/message event per non-cancelled call. A request with _meta.progressToken can receive notifications/progress through the optional async notification_sender; duplicate active tokens are rejected. It negotiates MCP 2025-11-25 and 2025-06-18. Application-level tool failures are successful JSON-RPC responses with isError: true; malformed protocol calls use standard JSON-RPC error objects. An MCP-cancelled call returns None and emits no response. Direct host task cancellation continues to raise asyncio.CancelledError.

With enable_tasks=True and MCP 2025-11-25, handle() also advertises task-augmented tools/call plus cancellation, emits per-tool execution.taskSupport, and handles tasks/get, blocking tasks/result, and tasks/cancel. Requested TTLs are positive finite milliseconds and are clamped to max_task_ttl_ms. Retained state and results are in memory, bounded by max_retained_tasks, and removed after their TTL. Arguments pass the runtime's resource preflight before being detached for background execution. tasks.list is intentionally unavailable without a requestor identity. Task IDs are cryptographically random; possession still grants access within that server session, so a network adapter must bind task operations to authenticated authorization context. aclose() cancels retained task executions before it optionally closes the runtime.

serve_stdio

await serve_stdio(
    server: MCPServer,
    *,
    input_stream: BinaryIO | None = None,
    output_stream: TextIO | None = None,
    max_message_bytes: int = 1_048_576,
    max_in_flight_requests: int = 64,
    close_runtime: bool = True,
)

Runs the server using newline-delimited MCP messages. max_message_bytes must be at least 256 and caps individual input and output messages. Tool calls and blocking task-result waits are dispatched concurrently so cancellation notifications remain responsive, with pending requests bounded separately by the positive max_in_flight_requests cap. Excess calls receive JSON-RPC server error -32000. The default streams are binary stdin and UTF-8 binary stdout. Only protocol messages are written to stdout.

Data models

All public models are frozen, slotted dataclasses.

  • ToolSpec: name, description, input/output schemas, timeout, version, tags, async state, optional title, read-only/destructive/idempotent/open-world hints, and MCP task-support mode.
  • ToolCall: name, arguments, and optional timeout override.
  • ToolPolicyContext: invocation ID plus detached validated arguments and tool spec; unlike result models, it intentionally has no serialization helper.
  • ToolPolicyDecision: explicit allow or deny policy outcome.
  • ToolPolicy: async policy callable type alias.
  • ToolResult: invocation ID, tool name, status, UTC start time, duration, output, and optional structured error. success is a convenience property.
  • ToolLifecycleEvent: immutable content-free runtime start/terminal signal with a JSON-compatible to_dict() representation.
  • ToolLifecycleStatus: started, every returned result status, cancelled, and aborted.
  • ToolLifecycleHandler: synchronous lifecycle callback type alias.
  • ToolError: code, safe message, optional exception type/details, and retryable flag.
  • ToolProgress: numeric progress, optional total, and optional human-readable message.
  • RuntimeMetrics: content-free counters only, including policy denials and runtime saturation.
  • ToolStatus: success, not_found, invalid_arguments, denied, busy, timed_out, failed, and runtime_closed.
  • TaskSupport: the "forbidden" | "optional" | "required" public type alias.

ToolSpec, ToolResult, ToolError, ToolLifecycleEvent, and RuntimeMetrics provide to_dict().

Supported annotations

  • scalars: str, bool, int, float, None
  • Literal with JSON-scalar values
  • unions and Optional
  • list[T]
  • fixed tuple[T1, T2] and variable tuple[T, ...]
  • dict[str, T]
  • TypedDict with strict named fields, nesting, inheritance, and total/optional key semantics
  • Annotated[T, "property description"]

Required and NotRequired are honored when their provider (typing on Python 3.11+ or an application-installed compatible backport) is available. Recursive TypedDict definitions are rejected during tool declaration so schema generation cannot recurse indefinitely. Tool invocations still pass and return ordinary dictionaries; Core does not construct user-defined objects.

Any, dataclasses, enums, custom classes, sets, arbitrary mappings, bytes, datetimes, and non-string dictionary keys are intentionally not part of the alpha contract. Integers do not accept booleans. Floats must be finite. Defaults and outputs are checked too.

Exceptions

Public definition/registry exceptions are SamsarixError, ToolDefinitionError, DuplicateToolError, RegistryCapacityError, and ToolNotFoundError. A host progress callback failure raises ProgressHandlerError with the original failure as its cause instead of misclassifying it as a tool result. Ordinary invocation failures are returned as ToolResult. asyncio.CancelledError propagates.

helix_core, helix_tool, and HelixError are compatibility aliases for the pre-rebrand alpha surface. They are not the preferred names for new code.