Skip to main content

toollint

PyPI version Python

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)

Source distribution for toollint 0.1.2
File Size Uploaded
toollint-0.1.2.tar.gz 46.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toollint 0.1.2
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.2 This release

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