Toolint
Structural linter for MCP-compatible, zero-dependency Python agent tool packages.
toolint enforces architectural rules that ensure your Python package works correctly as:
- A standalone library (
from my_tool import MyTool) - A CLI tool (
my-tool search "query") - An MCP server (
my-tool serve --source spec.json) - An SDK middleware (OpenAI / Anthropic client patches)
Inspired by the architecture of graph-tool-call.
Why?
Building agent-compatible tools is easy to get wrong:
| Mistake | Consequence |
|---|---|
core/ imports numpy without guard |
Users without numpy get ImportError on import my_tool |
| MCP server has business logic | Can't use the same functionality as a library |
__version__ != pyproject.toml version |
PyPI shows wrong version |
| Tool function has no docstring | LLM can't understand what the tool does |
Optional dep not in extras |
pip install my-tool[mcp] doesn't install MCP SDK |
toolint catches all of these before they reach users.
Installation
pip install toolint
# or with uv
uv pip install toolint
# or as a tool
uvx toolint check .
Quick Start
# Lint current project
toolint check .
# Lint with specific rules only
toolint check . --select ATL101,ATL102
# Ignore specific rules
toolint check . --ignore ATL105
# JSON output (for CI integration)
toolint check . --format json
# Show all available rules
toolint rules
Example Output
my_tool/core/engine.py:3:0 ATL101 (error)
Hard import of 'numpy' in core module — core must be stdlib-only.
Use try/except ImportError guard or move to a non-core module.
my_tool/retrieval/embedding.py:5:0 ATL102 (error)
Optional import 'sentence_transformers' missing try/except ImportError guard.
my_tool/__init__.py:1:0 ATL004 (error)
Version mismatch: __init__.py has '0.2.0' but pyproject.toml has '0.2.1'
my_tool/mcp_server.py:42:8 ATL503 (error)
MCP tool function 'process_data' has no docstring.
LLMs rely on tool descriptions to select the right tool.
4 issues found (4 errors, 0 warnings)
Rules
Layer 1: Structure (ATL0xx)
| Rule | Severity | Description |
|---|---|---|
ATL001 |
error | Package must have a single public facade class |
ATL002 |
error | __main__.py must exist and be registered in pyproject.toml scripts |
ATL003 |
warning | __init__.py must define __all__ including the facade class |
ATL004 |
error | __version__ in __init__.py must match pyproject.toml version |
Layer 2: Dependency Rules (ATL1xx)
| Rule | Severity | Description |
|---|---|---|
ATL101 |
error | No third-party imports in core/ directory (stdlib only) |
ATL102 |
error | Optional dependencies must use try/except ImportError guard |
ATL103 |
warning | Import guard must include install hint (e.g. pip install pkg[extra]) |
ATL104 |
error | Optional imports must be registered in pyproject.toml extras |
ATL105 |
warning | __init__.py should not eagerly import optional-dep modules |
Layer 3: Layer Separation (ATL2xx)
| Rule | Severity | Description |
|---|---|---|
ATL201 |
warning | Interface files should not call internal business logic directly (type/enum/constant imports are allowed) |
ATL202 |
warning | CLI command handlers should invoke functionality through the facade class |
ATL203 |
warning | Interface layer should not import core/ internals directly (except types/constants) |
Layer 4: pyproject.toml Consistency (ATL3xx)
| Rule | Severity | Description |
|---|---|---|
ATL301 |
error | CLI entry point must be registered in [tool.poetry.scripts] or [project.scripts] |
ATL302 |
error | If MCP server exists, mcp extras group must be defined |
ATL303 |
warning | all extras group must include all dependencies from other extras groups |
Layer 5: Tool Schema Quality (ATL5xx)
| Rule | Severity | Description |
|---|---|---|
ATL501 |
warning | Facade public methods must have docstrings |
ATL502 |
warning | Facade public methods must have parameter + return type hints |
ATL503 |
error | MCP tool functions must have docstrings (min 10 chars) |
ATL504 |
warning | MCP tool function docstrings should describe each parameter |
Layer 6: Agent Compatibility (ATL6xx)
| Rule | Severity | Description |
|---|---|---|
ATL601 |
warning | Facade public methods should return JSON-serializable types |
ATL602 |
error | MCP tool functions must return str (MCP protocol requirement) |
ATL603 |
warning | Facade/MCP tools should not silently swallow exceptions |
Configuration
Add to pyproject.toml:
[tool.toolint]
# Package root (auto-detected from pyproject.toml)
package = "my_tool"
# Facade class name (auto-detected if single prominent class exists)
facade_class = "MyTool"
# Core directory (default: "core")
core_dir = "core"
# Interface files (default: auto-detected)
interface_files = ["mcp_server.py", "mcp_proxy.py", "middleware.py", "__main__.py"]
# Extra stdlib-like packages allowed in core (escape hatch)
core_allowed_imports = []
# Rules to ignore
ignore = ["ATL105"]
Or use a standalone file .toolint.toml with the same structure (without the [tool.toolint] nesting).
The Architecture This Enforces
my_package/
├── __init__.py # __version__, __all__, lazy imports
├── __main__.py # CLI (argparse) — calls facade only
├── core/ # ZERO external dependencies (stdlib only)
│ ├── protocol.py # Abstract interfaces (Protocol classes)
│ └── models.py # Domain models (dataclasses)
├── feature_a/ # Business logic modules
│ └── ... # May use optional deps with import guards
├── facade.py # Single public API class
├── mcp_server.py # MCP server — wraps facade only
└── middleware.py # SDK patches — wraps facade only
Key principles:
- Core is stdlib-only — anyone can
import my_toolwithout installing extras - Facade is the single API surface — all interfaces (CLI, MCP, middleware) go through it
- Optional deps use import guards — graceful degradation, not crashes
- Interface layers are thin wrappers — no business logic in MCP server or CLI
CI Integration
GitHub Actions
- name: Lint agent tool structure
run: uvx toolint check .
Pre-commit
repos:
- repo: https://github.com/PlateerLab/toolint
rev: v0.1.0
hooks:
- id: toolint
Technical Details
- Python 3.10+
- Zero dependencies — uses only
astandtomllibfrom stdlib - Fast — AST parsing, no runtime imports of the target package
- Self-validating —
toolintfollows the same architecture it enforces
License
MIT
Links
- GitHub
- PyPI (coming soon)
- graph-tool-call — the reference implementation this linter is based on
Metadata
Release files for toolint 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| toolint-0.1.0.tar.gz | 22.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| toolint-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 49.6 kB
Release files / toolint-0.1.0.tar.gz
| Download URL | toolint-0.1.0.tar.gz |
|---|---|
| Size | 22.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6db1de54ea3c9efafe8875e3a357ff68830ede56d52619f41773eaef00d8b7fd
|
|
BLAKE2b-256 checksum How to use checksums |
57339d34074e8067960f67fc50b303a3fbab8b2565b8dfc557f0ee37d3aa78b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / toolint-0.1.0-py3-none-any.whl
| Download URL | toolint-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
322edd16b2c5f14b790fb6376429e17767e76ae65abc3f19c17f58346d1f9a15
|
|
BLAKE2b-256 checksum How to use checksums |
7c24a48c8605b477d56ffcc2979e510ea8c72fb2a1a62a62f4a799c1c24e1e07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|