toollint
Lint tool definitions for AI agents. Checks descriptions, parameter docs, and semantic overlap so models pick the right tool.
Install
pip install toollint
# With semantic overlap detection:
pip install toollint[semantic]
Requires Python >= 3.10.
Quick start
toollint --src ./src/ # scan + lint in one command
toollint build --src ./src/ # just collect, write _build/tools.json
toollint check # lint _build/tools.json
toollint check --format json # CI-friendly output
toollint check --quiet # no output on clean runs
cat tools.json | toollint check --stdin # read from pipe
Defining tools
Three ways to define tools. Mix and match.
Option A: @tool decorator
from toollint import tool
@tool
def search_web(query: str, max_results: int = 10) -> str:
"""Search the web for current information.
Args:
query: The search query string.
max_results: Maximum number of results.
"""
...
# Override name and description via decorator kwargs
@tool(name="web_search", description="Search the internet")
def search(query: str) -> str:
...
# Async functions work too
@tool
async def fetch_data(url: str) -> dict:
"""Fetch data from a URL.
Args:
url: The URL to fetch.
"""
...
The @tool decorator is an identity function at runtime — zero overhead. The AST scanner finds it statically. Only matches @tool imported from toollint; from otherlib import tool is ignored.
Docstrings are parsed in Google, Sphinx, and NumPy styles. First match wins.
Option B: Tool base class
from toollint import Tool
class SearchDocs(Tool):
name = "search_docs"
description = "Search documentation for a query string"
def run(self, query: str, max_results: int = 10) -> str:
"""Run the search.
Args:
query: The search query.
max_results: Maximum results.
"""
...
# with type annotations preserved
from typing import Optional, Literal
class CreateFile(Tool):
name = "create_file"
description = "Create a new file with the given content"
def run(self, path: str, content: str, mode: Literal["write", "append"] = "write") -> str:
"""Create a file.
Args:
path: File path.
content: File content.
mode: Write mode.
"""
...
The AST scanner finds classes inheriting from toollint.Tool. Extracts name and description class attributes, then run() method signature for parameters. self is automatically excluded from the parameter list.
Only matches Tool imported from toollint. from otherlib import Tool is ignored.
Option C: tools/*.json
{
"name": "extra_tool",
"description": "An extra tool from JSON config",
"parameters": [
{
"name": "query",
"type": "str",
"description": "The search query",
"required": true
}
]
}
Hand-written JSON definitions. Files can be a JSON array or NDJSON (one object per line). All *.json files in tools/ are merged at build time. Duplicate names: last file wins with a warning.
Public API
Everything importable from toollint root. Internal modules start with _ — not part of the public contract.
Classes
ToolDef
A complete tool definition. The central data structure.
from toollint import ToolDef, ParameterDef
tool = ToolDef(
name="search_web",
description="Search the web for current information",
source_file="src/search.py",
source_line=42,
parameters=[
ParameterDef(
name="query",
type="str",
description="The search query string",
required=True,
),
ParameterDef(
name="max_results",
type="int",
required=False,
),
],
)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Tool name |
description |
str | None |
None |
Tool description. None triggers "missing description" error |
source_file |
str | None |
None |
Source file path for clickable output |
source_line |
int | None |
None |
Source line number |
parameters |
list[ParameterDef] |
[] |
Parameter list, nested arbitrarily |
ParameterDef
A parameter within a tool definition.
from toollint import ParameterDef
param = ParameterDef(
name="query",
type="str",
description="The search query",
required=True,
parameters=[], # nested params, e.g. for object/dict types
)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Parameter name |
type |
str |
"Any" |
Type string, e.g. "str", "int", "Optional[str]" |
description |
str | None |
None |
Parameter description. None triggers warning |
required |
bool |
True |
Whether the parameter is required |
parameters |
list[ParameterDef] |
[] |
Nested parameters (dict/object children) |
Report
Result of running checks. Returned by scan().
from toollint import scan, ToolDef
tools = [ToolDef(name="a", description="..."), ToolDef(name="b", description="...")]
report = scan(tools)
print(report.ok) # bool: True if no errors
print(report.errors) # list[Finding]: error-level findings
print(report.warnings) # list[Finding]: warning-level findings
print(report.skipped) # list[str]: skipped check names
Finding
A single lint issue. Returned in report.errors and report.warnings.
finding = report.errors[0]
finding.check # str: check name, e.g. "duplicate_names"
finding.severity # Severity enum: ERROR or WARNING
finding.message # str: human-readable description
finding.tools # list[str]: tool names involved
finding.files # list[str]: source files involved
Decorators and base classes
tool
Runtime no-op decorator. AST scanner finds it statically.
from toollint import tool
@tool
def my_tool(x: str) -> str:
"""Description here.
Args:
x: The parameter.
"""
...
@tool(name="custom_name", description="Override everything")
def other(y: int) -> str:
...
Tool
Abstract base class. Subclass and set name + description class attributes.
from toollint import Tool
class MyTool(Tool):
name = "my_tool"
description = "What this tool does"
def run(self, input: str) -> str:
...
Functions
scan(tools, threshold=0.85) -> Report
Run all checks on a list of ToolDef objects. Returns a Report.
from toollint import scan, ToolDef
tools = [
ToolDef(name="a", description="Search the web for info"),
ToolDef(name="b", description="Look up things on the internet"),
]
report = scan(tools)
if not report.ok:
for finding in report.errors:
print(f"ERROR: {finding.message}")
| Parameter | Type | Default | Description |
|---|---|---|---|
tools |
list[ToolDef] |
required | Tool definitions to check |
threshold |
float |
0.85 |
Cosine similarity threshold for overlap detection |
tools_from_json(path) -> list[ToolDef]
Load tool definitions from a JSON file. Handles both JSON arrays and NDJSON.
from toollint import tools_from_json
from pathlib import Path
tools = tools_from_json(Path("_build/tools.json"))
Severity
from toollint._engine._checks import Severity
Severity.ERROR # "error"
Severity.WARNING # "warning"
Import from toollint._engine._checks — intentionally internal. Consumers read finding.severity on the Finding object, don't construct Severity values directly.
Checks
| Check | Severity | Trigger |
|---|---|---|
semantic_overlap |
Error | Two descriptions have cosine similarity > threshold (default 0.85) |
duplicate_names |
Error | Two tools share the exact same name |
missing_description |
Error | Tool has description=None or "" |
required_after_optional |
Error | A required parameter follows an optional one (OpenAI/Anthropic reject this schema) |
short_description |
Warning | Description has < 10 words OR doesn't start with an action verb |
missing_param_description |
Warning | Any parameter at any nesting level has no description |
Semantic overlap requires pip install toollint[semantic]. All other checks run with zero ML dependencies.
CLI reference
toollint --src ./src/ build + check (default)
toollint build --src ./src/ collect tools, write _build/tools.json
toollint check lint _build/tools.json
toollint check tools.json lint a specific file
toollint check --stdin read JSON array or NDJSON from pipe
| Flag | Applicable to | Default | Description |
|---|---|---|---|
--src PATH |
default, build |
./src/ |
Python source directory to scan |
--format rich|json |
default, check |
rich |
Output format |
--quiet |
default, check |
off | Suppress output when no errors |
--threshold FLOAT |
default, check |
0.85 |
Similarity threshold |
Exit codes: 0 = clean or warnings only, 1 = errors found, 2 = configuration error.
CI integration
# .github/workflows/toollint.yml
name: toollint
on: [push, pull_request]
jobs:
lint-tools:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install toollint[semantic]
- run: toollint --src ./src/
Pre-commit hook:
# .pre-commit-config.yaml
- repo: local
hooks:
- id: toollint
name: toollint
entry: toollint
language: system
pass_filenames: false
License
Apache-2.0
Release files for toollint 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| toollint-0.1.2.tar.gz | 46.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| toollint-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 69.5 kB
Release files / toollint-0.1.2.tar.gz
| Download URL | toollint-0.1.2.tar.gz |
|---|---|
| Size | 46.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c38477f338769a1e86dd5a8709de1d2d7dd17ef0586a6dc34bc4805adce94acc
|
|
BLAKE2b-256 checksum How to use checksums |
c118240ccace9baaf7503f902a99e556ceea7dd51ff2ff6c76b932eb45d93be3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.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 Jul 19, 2026.
Transparency logRelease files / toollint-0.1.2-py3-none-any.whl
| Download URL | toollint-0.1.2-py3-none-any.whl |
|---|---|
| Size | 23.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fa3ed1d48f2fe44b236cfc8520ff2bdf14970e0890645ed042e608159272bae9
|
|
BLAKE2b-256 checksum How to use checksums |
5f2daef4a617980fa799a16ec0ae35078dafbcd9ab697403fd0255193478090e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.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 Jul 19, 2026.
Transparency log