Skip to main content

toolfuncs

toolfuncs is a small function-calling convention for Python tools. A tool is a Python module whose typed public functions are its interface. The same functions can be called directly from Python or projected into a command-line interface without maintaining a second wrapper API.

Discoverable tools live in a project or user-wide tool directory in one of two forms: a PEP 723 Python file or one packaged src project layout. The separate import_path() utility can load broader Python sources from local paths and fsspec URLs.

The basic model

Write ordinary typed functions and register the CLI-callable operations on toolfuncs.App:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["toolfuncs"]
# [tool.toolfuncs]
# description = "Render values for another program."
# ///

from pathlib import Path

from toolfuncs import App

app = App()


@app.command
def render(value: str, *, output: Path | None = None) -> dict[str, object]:
    """Return a value and its optional output path."""

    return {"value": value, "output": output}


if __name__ == "__main__":
    app()

The Python interface is the definition:

from pathlib import Path

from toolfuncs import renderer

result = renderer.render("hello", output=Path("result.txt"))

The CLI is derived from that same signature and returns strict JSON:

$ toolfuncs renderer render hello --output result.txt
{"value": "hello", "output": "result.txt"}

toolfuncs directly depends on Cyclopts and pydantic-core. Its App is a Cyclopts app with the established strict-JSON result action as its default, so a tool imports only toolfuncs rather than importing and configuring those libraries itself. Parameter, Token, Group, validators, types, and to_jsonable_python are also re-exported for tools that need the corresponding advanced behavior. Passing result_action= explicitly preserves Cyclopts' ordinary override behavior for streaming or domain-specific status policies.

The decorator registers the original function without wrapping it. Direct Python calls therefore receive the original object and exceptions. CLI calls parse annotated values, emit one JSON value on stdout, and fail nonzero when parsing, execution, or serialization fails.

Scoped tools

toolfuncs discovers tools from two roots:

  1. the nearest .agents/tools directory at or above the process's initial working directory;
  2. ~/.agents/tools.

An immediate child defines a tool only when it has one of these exact shapes:

name.py                              # PEP 723 script metadata required

name/
  pyproject.toml
  src/
    name/
      __init__.py

Both metadata containers require one static field:

[tool.toolfuncs]
description = "Describe this tool in one line."

The table is the explicit discovery marker; files and directories without it are ignored. This permits unrelated launchers, hook handlers, and other implementation programs to coexist in the directory without becoming agent tools. For the file form, the table is inside the PEP 723 script block. For the packaged form, pyproject.toml must also define a matching normalized [project].name and an explicit [build-system]. Once declared, the tool must satisfy the complete shape: its directory name, scoped name, and Python import name are the same valid non-keyword identifier. Flat projects, direct package directories, single-module projects, namespace roots, legacy setup.py projects, artifacts, and remote URLs are not discoverable tool forms.

A project tool shadows a user tool with the same name. The roots are captured when toolfuncs is first imported, so a long-running interpreter does not silently change its tool universe after chdir().

This supports the portable dynamic-import form:

from toolfuncs import codexr

agents = codexr.list_agents()

from toolfuncs import name is intentionally the portable form. import toolfuncs.name is not promised because scoped resolution is a dynamic package attribute rather than an installed toolfuncs submodule. Use load_tool("name") when a tool name conflicts with a toolfuncs API member.

Install the dispatcher and direct command shims with one command:

uvx toolfuncs setup

setup writes a small toolfuncs wrapper to ~/.local/bin that runs the latest available package through uvx, then reconciles shims for the currently visible tools. No persistent uv tool environment is required. Universal CLI dispatch then works through that wrapper:

toolfuncs codexr list-agents
toolfuncs run codexr list-agents

Run toolfuncs sync later to add direct command shims for newly visible tools, or choose another directory during either command with --bin-dir. A shim performs scoped lookup again when invoked, so the same command correctly resolves project overrides. setup and sync never overwrite an unmanaged command and do not remove older managed shims.

Composing tools

A tool can import another scoped tool and call it like any Python module:

from toolfuncs import codexr
from toolfuncs import App

app = App()


@app.command
def active_agent_ids() -> list[str]:
    return [agent.session_id for agent in codexr.list_agents()]

Imported or undecorated functions are not exposed accidentally. Only functions registered on app become CLI commands; __all__ retains its ordinary Python export meaning but does not define the command surface.

Importing arbitrary sources

import_path() handles the following v1 source forms:

Source Behavior
tool.py executes directly; reads optional PEP 723 metadata
package/__init__.py imports package directly
package/ containing __init__.py imports directly with relative imports and resources
project containing pyproject.toml builds one wheel, installs it, then imports its selected module
project containing setup.py builds one wheel through pip's legacy-project support
.whl inspects, installs, and imports the wheel
source archive such as .tar.gz or .zip builds, installs, and imports one wheel
any of the above behind an fsspec URL materializes the complete source and applies the same classification

Direct packages may declare dependencies in an adjacent pyproject.toml [project] table. PEP 723 metadata is read only from single Python files. Projects and artifacts leave dependency handling to their build metadata and pip.

Regular projects are interpreted by their build backend rather than guessed from their directory tree. The resulting wheel is inspected for actual top-level Python imports. This supports conventional flat and src layouts for both modules and packages, as well as installed namespace packages. A bare src/__init__.py is not treated as a package layout.

Import selection follows this order:

  1. explicit import_name=;
  2. a project directory name that matches an import in the built wheel;
  3. the wheel's sole top-level import.

Ambiguous projects fail with their discovered candidates:

from toolfuncs import import_path

pillow = import_path("./vendor/Pillow", import_name="PIL")

import_name= belongs only to this generic utility. Discoverable packaged tools never configure or infer their import name; the scoped name is passed to import_path() internally.

Identity and execution environment

Within one interpreter, a canonical source and import name identify one module object. Re-importing the same source returns that object, one source cannot be assigned two names, and one name cannot be assigned two sources. Direct dotted imports construct missing namespace parents, support relative imports, clean up after failed execution, and work with importlib.reload().

V1 installs dependencies and project wheels into the running interpreter environment under a process-wide import lock. This is deliberate: direct Python calls must receive the real module and Python objects rather than a proxy transport. It also means dependency conflicts are ordinary environment conflicts. Use a dedicated environment when mutually incompatible tools must coexist.

Credentials in source URLs and opaque URL queries are removed from user-facing diagnostics. storage_options= is passed directly to fsspec and is never incorporated into module names.

Management commands

toolfuncs list
toolfuncs describe TOOL
toolfuncs setup [--bin-dir DIRECTORY]
toolfuncs sync [--bin-dir DIRECTORY]
toolfuncs TOOL [ARGS...]

list, describe, and sync emit strict JSON. list and describe read only static TOML metadata and never import tools, resolve their dependencies, or run build code. Their records contain name, path, scope, kind, and description. Use toolfuncs TOOL --help when runtime command inspection is needed.

Development

uv sync
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv run basedpyright

The detailed v1 guarantees and non-goals are recorded in the contract.

Releasing

The GitHub Release is the release control point. Set [project].version, merge and push that commit, then publish a GitHub Release whose tag is v<version>. The release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through the pypi environment and PyPI Trusted Publishing. It uses no long-lived PyPI token or repository secret.

Metadata

Release files for toolfuncs 0.1.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toolfuncs 0.1.4
File Size Uploaded
toolfuncs-0.1.4.tar.gz 125.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolfuncs 0.1.4
File Interpreter ABI Platform
toolfuncs-0.1.4-py3-none-any.whl Python 3 none any Details

Total release size: 146.8 kB

Release files / toolfuncs-0.1.4.tar.gz

Download URL toolfuncs-0.1.4.tar.gz
Size 125.3 kB
Tags Source
SHA-256 checksum
How to use checksums
42fe7ce676b97e9ef9c9e88b7702067a3de553a6b7ff828e90d5960adce653d9
BLAKE2b-256 checksum
How to use checksums
268739538ec6ca239efea8ffd3aa1986c544b0109df8abb7c2d505a944aee444
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release files / toolfuncs-0.1.4-py3-none-any.whl

Download URL toolfuncs-0.1.4-py3-none-any.whl
Size 21.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c3ff5d7f2759b47145dd4f863dd6eed8e1d6bfd7f82c8079d8c387b6f969dcd0
BLAKE2b-256 checksum
How to use checksums
1e957fef698d644787b220f6e26c928d0fea0d56dc794ef5bc9b7709de1c65bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.12.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

This release

0.1.4 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page