toolfuncs
toolfuncs turns typed Python functions into tools that have matching Python and command-line interfaces.
A toolfunc is one executable Python script. Its lowercase kebab-case filename is its command name, and its Python import name is the same spelling with hyphens replaced by underscores. A known source can be called or imported directly from any path. Putting it on PATH additionally makes its filename a shell command and exposes it to discovery and dynamic Python lookup. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves toolfuncs.
Write a toolfunc
Create an extensionless file named weather-report:
#!/usr/bin/env toolfuncs
# /// script
# requires-python = ">=3.10"
# dependencies = ["weather-client"]
# [tool.toolfuncs]
# description = "Read current weather conditions."
# cli_name = "weather-report"
# python_name = "weather_report"
# ///
import toolfuncs.sdk as toolsdk
import weather_client
app = toolsdk.App()
@app.command
def current(city: str) -> dict[str, object]:
"""Read current conditions for one city.
Parameters
----------
city:
City whose current conditions should be read.
"""
return weather_client.current(city)
Make it executable:
chmod +x weather-report
No registration operation is required. Move it to an existing PATH directory only when it should be globally discoverable:
mv weather-report ~/.local/bin/
Call it
A known source can be run explicitly without adding it to PATH:
$ toolfuncs ./weather-report current London
{"city": "London", "temperature": 18}
When the source is on PATH, its filename is also the CLI:
$ weather-report current London
{"city": "London", "temperature": 18}
A known source is importable by path:
import toolfuncs as tools
weather_report = tools.import_path("./weather-report")
conditions = weather_report.current("London")
When the source is on PATH, the same file is also dynamically importable by name in a Python process with that PATH:
import toolfuncs as tools
conditions = tools.weather_report.current("London")
import_path() also works for an ordinary local Python script whose path is already known:
import toolfuncs as tools
module = tools.import_path("scripts/prepare_data.py")
It accepts one ordinary .py file or extensionless script and derives the module name from its filename. An extensionless lowercase kebab-case filename is mapped to its snake-case Python name. It does not require a shebang, executable bit,
[tool.toolfuncs] metadata, or App, and it does not import packages, projects,
distributions, or URLs. When an optional PEP 723 block exists, the default
installer installs all declared requirements together into the running Python
environment before import. A caller can replace that behavior with one callable
that accepts list[str] and returns None:
module = tools.import_path(
"scripts/prepare_data.py",
dependency_installer=my_installer,
)
Direct Python calls return ordinary Python objects and raise the original exceptions. Dynamic tool attributes and toolfuncs.import_path() prepare declared requirements in the running Python environment before import. CLI calls use Cyclopts to parse annotated values and serialize successful results as strict JSON through pydantic-core.
Source contract
A toolfunc source must:
- be one executable file;
- have an extensionless lowercase ASCII kebab-case filename;
- start with one of the two exact toolfuncs shebangs described below;
- contain exactly one PEP 723
scriptblock withdependencies, a one-line description, and requiredcli_nameandpython_namedocumentation that exactly matches the filename-derived identities; - define a module-level
app = toolsdk.App()afterimport toolfuncs.sdk as toolsdk; - give the module and every registered operation a docstring, annotate every operation parameter and return value, and provide effective Cyclopts help for every visible CLI argument;
- register each CLI-callable function explicitly with
@app.command.
The PEP 723 dependency list contains the packages needed in addition to the running toolfuncs environment. Listing toolfuncs itself is accepted but normally redundant because the shebang has already started the toolfuncs runtime before dependencies are prepared.
Git requirements must use a full 40- or 64-hex commit object ID. Branches such as @main, tags, abbreviated hashes, and omitted revisions fail before uv resolves them. A tool that deliberately accepts floating-ref refresh and concurrency costs must say so in its source:
#!/usr/bin/env -S toolfuncs --allow-floating-vcs
The launcher consumes --allow-floating-vcs; the tool's Cyclopts app never sees it. Adjacent uv lock --script files are not used because uv does not consult them for --with-requirements launches.
Only functions registered on app become CLI commands. Imported functions and __all__ do not define the command surface. Function command and option spelling follows Cyclopts' Python-to-kebab-case projection.
No if __name__ == "__main__": block is required. The operating system passes the source path to the shebang interpreter, and toolfuncs imports the source under its declared Python name before invoking its module-level app. Consequently, weather-report ... is the supported CLI while python weather-report ... merely defines the module and exits, or fails if its dependencies are not already installed.
Package-backed implementations
A toolfunc remains one script even when most of its implementation lives in a package:
#!/usr/bin/env toolfuncs
# /// script
# requires-python = ">=3.10"
# dependencies = ["my-large-package"]
# [tool.toolfuncs]
# description = "Run the package's agent operation."
# cli_name = "package-operation"
# python_name = "package_operation"
# ///
from my_large_package.agent_tool import app, perform_operation
The imported app and functions are the original Python objects. Toolfuncs does not inspect the package's project layout, metadata, or import structure.
Discovery and precedence
Toolfuncs reads the current process's PATH from left to right. It considers only executable files and symlinks, reads their first line, and parses PEP 723 metadata only when either exact toolfuncs shebang matches.
The first executable filename claims a command name even when it is not a toolfunc. Therefore an ordinary executable earlier on PATH hides a same-named toolfunc later on PATH, matching what the shell actually executes. Repeated directories are scanned once, inaccessible directories are skipped, and discovered records are sorted by CLI name.
toolfuncs list returns one JSON object with tools and invalid_tools. Valid records contain cli_name, python_name, path, and description. An opted-in but invalid tool is reported with its path and validation error without hiding unrelated valid tools; the command also prints a concise warning to stderr and exits successfully. Listing never imports a tool, prepares its dependencies, or executes its source.
Validate a tool while authoring
Run the doctor against one explicit source before installing or committing it:
toolfuncs doctor ./weather-report
The doctor validates the static source contract, prepares the declared dependencies, loads the source, and inspects the actual Cyclopts command graph. It requires a module docstring, at least one registered operation, a docstring and complete annotations for every operation, effective help text for every visible parsed CLI argument, and successful root and command help generation. Aliases are inspected once through their underlying command.
The result is always one strict-JSON tagged union. A valid source exits zero:
{"status": "success", "path": "/path/to/weather-report", "commands": ["current"]}
Expected authoring failures return status: "error", list every independently detectable operation error, and exit nonzero without using an exception as the result:
{"status": "error", "path": "/path/to/weather-report", "errors": ["current: CLI argument '--city' must have help text"]}
Loading is intentional: the registered command graph and Cyclopts' effective parameter help can be constructed dynamically and should not be approximated with a second source parser. Run the doctor in a suitable disposable environment when dependency isolation matters.
Install the runtime
uvx toolfuncs setup
Setup writes two managed, auto-updating uvx wrappers by default:
~/.local/bin/toolfuncsdispatches tools;~/.agents/hooks/toolfuncs/hookadvertises the current tool catalog to agent harnesses.
For a tool invocation, its effective launch is:
#!/bin/sh
# managed by toolfuncs setup
exec uvx --quiet --from toolfuncs --with-requirements "$source" \
toolfuncs __run-source "$source" "$@"
The wrapper preflights Git requirements before that command unless the source explicitly allows floating VCS revisions. Normal uv cache freshness supplies updates without forcing --refresh-package on every call. The destination must already be on PATH; setup fails with a concrete instruction otherwise. Setup is idempotent, refuses to overwrite unmanaged commands, and does not modify shell profiles.
The hook wrapper resolves the optional toolfuncs[hooks] environment and is registered at user scope for Codex and Claude Code through typed-agent-hooks. Running setup again reconciles those registrations, so the managed launcher remains the configured executable even when provider configuration changes.
Setup does not discover, copy, register, link, synchronize, or remove tools. There is no dedicated tools directory, registry, manifest, generated shim, project scope, user scope, or sync command.
Management commands
toolfuncs SOURCE [ARGS...]
toolfuncs doctor SOURCE
toolfuncs list
toolfuncs setup [--bin-dir DIRECTORY]
toolfuncs SOURCE [ARGS...] runs a known source directly. This is the explicit-source interface, not a name dispatcher: toolfuncs weather-report resolves weather-report as a source path rather than searching PATH for that tool name. A toolfunc on PATH can instead be invoked directly by its own filename.
Agent integration
The installed hook runs at initial session start and at the session-start event emitted after compaction. It discovers the effective tools directly from that harness process's PATH, then adds a compact catalog such as:
Available toolfuncs:
- `weather_report`: Read current weather conditions.
CLI: `weather-report --help`
Python: `import toolfuncs as tools; help(tools.weather_report)`
The hook reads only executable headers and PEP 723 metadata. It advertises valid tools and lists invalid tool paths and errors separately; one invalid tool does not suppress the others. It does not import tools or resolve their dependencies. The hook itself is harness infrastructure, not a toolfunc, and therefore does not carry the toolfuncs shebang or appear in the catalog.
Development
uv sync
uv run pytest
uv run ruff format --check .
uv run ruff check .
uv run basedpyright
The exact 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 self-hosted release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through Trusted Publishing.
Metadata
Release files for toolfuncs 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| toolfuncs-0.7.0.tar.gz | 68.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| toolfuncs-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 90.7 kB
Release files / toolfuncs-0.7.0.tar.gz
| Download URL | toolfuncs-0.7.0.tar.gz |
|---|---|
| Size | 68.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ad48e85301f476668039f330b3282e4c0414b6a75b9ec9b22c56726ef29c9b74
|
|
BLAKE2b-256 checksum How to use checksums |
6a2d09a0978dfce80a68f9beff8fb737942cf43e813c39e41c23700d83c5b7aa
|
| 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 Sep 1, 2026.
Transparency logRelease files / toolfuncs-0.7.0-py3-none-any.whl
| Download URL | toolfuncs-0.7.0-py3-none-any.whl |
|---|---|
| Size | 22.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
32df1ac49c20b25ba6fd55cf0f5f6f257fef8f68a6e61052c0db4612b1cbd748
|
|
BLAKE2b-256 checksum How to use checksums |
bd24ca1ac7d8c76c1fe356b380d0f1ea5211ca4b2b1190ee3f861eea20f03dc7
|
| 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 Sep 1, 2026.
Transparency log