Skip to main content

Lint tool definitions for AI agents

Project description

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

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.1.tar.gz (46.2 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.1-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: toollint-0.1.1.tar.gz
  • Upload date:
  • Size: 46.2 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.1.tar.gz
Algorithm Hash digest
SHA256 65ad3ad3a42d646327d8bd4ae479bca9fdaee20218c36896aa12b59f63746d59
MD5 23ca188e4370a50afe9f1a9c56523fea
BLAKE2b-256 29a40a0cbc984a9bbb1f4ab947195e9c011702e5f16906853f09865fe845403c

See more details on using hashes here.

Provenance

The following attestation bundles were made for toollint-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: toollint-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 22.9 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e8bad7d7910af87de01a8c4cbbbe1893a670db32f22dc1699c0198d1621d1da8
MD5 9029dc69e25f2714afe6f8809c1bf8d0
BLAKE2b-256 2de8b84fed763182789d27ec3a5833303213d2b787025862fdbf3de3cd299aaf

See more details on using hashes here.

Provenance

The following attestation bundles were made for toollint-0.1.1-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