Metadata-Version: 2.4
Name: toollint
Version: 0.1.2
Summary: Lint tool definitions for AI agents
License: Apache-2.0
Project-URL: Homepage, https://github.com/Sheerabth/toollint
Project-URL: Repository, https://github.com/Sheerabth/toollint
Project-URL: Issues, https://github.com/Sheerabth/toollint/issues
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: rich
Provides-Extra: semantic
Requires-Dist: sentence-transformers; extra == "semantic"
Requires-Dist: scikit-learn; extra == "semantic"
Dynamic: license-file

# toollint

[![PyPI version](https://img.shields.io/pypi/v/toollint)](https://pypi.org/project/toollint/)
[![Python](https://img.shields.io/pypi/pyversions/toollint)](https://pypi.org/project/toollint/)

Lint tool definitions for AI agents. Checks descriptions, parameter docs, and semantic overlap so models pick the right tool.

## Install

```bash
pip install toollint
# With semantic overlap detection:
pip install toollint[semantic]
```

Requires Python >= 3.10.

## Quick start

```bash
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

```python
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

```python
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`

```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.

```python
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.

```python
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()`.

```python
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`.

```python
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.

```python
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.

```python
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`.

```python
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.

```python
from toollint import tools_from_json
from pathlib import Path

tools = tools_from_json(Path("_build/tools.json"))
```

### Severity

```python
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

```yaml
# .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:

```yaml
# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: toollint
      name: toollint
      entry: toollint
      language: system
      pass_filenames: false
```

---

## License

Apache-2.0
