Skip to main content

Lint tool definitions for AI agents

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toollint-0.1.2.tar.gz (46.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toollint-0.1.2-py3-none-any.whl (23.0 kB view details)

Uploaded Python 3

File details

Details for the file toollint-0.1.2.tar.gz.

File metadata

  • Download URL: toollint-0.1.2.tar.gz
  • Upload date:
  • Size: 46.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for toollint-0.1.2.tar.gz
Algorithm Hash digest
SHA256 c38477f338769a1e86dd5a8709de1d2d7dd17ef0586a6dc34bc4805adce94acc
MD5 21f7d992cb893f5ea5d44444e90c01b8
BLAKE2b-256 c118240ccace9baaf7503f902a99e556ceea7dd51ff2ff6c76b932eb45d93be3

See more details on using hashes here.

Provenance

The following attestation bundles were made for toollint-0.1.2.tar.gz:

Publisher: ci.yml on Sheerabth/toollint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file toollint-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: toollint-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 23.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for toollint-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 fa3ed1d48f2fe44b236cfc8520ff2bdf14970e0890645ed042e608159272bae9
MD5 1f41924997c3327e52e78a83a769abd1
BLAKE2b-256 5f2daef4a617980fa799a16ec0ae35078dafbcd9ab697403fd0255193478090e

See more details on using hashes here.

Provenance

The following attestation bundles were made for toollint-0.1.2-py3-none-any.whl:

Publisher: ci.yml on Sheerabth/toollint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page