Structural linter for MCP-compatible, zero-dependency Python agent tool packages
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file toolint-0.1.0.tar.gz.
File metadata
- Download URL: toolint-0.1.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6db1de54ea3c9efafe8875e3a357ff68830ede56d52619f41773eaef00d8b7fd
|
|
| MD5 |
d1cb559e11688ed73fcb8ebccba8137c
|
|
| BLAKE2b-256 |
57339d34074e8067960f67fc50b303a3fbab8b2565b8dfc557f0ee37d3aa78b8
|
File details
Details for the file toolint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: toolint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
322edd16b2c5f14b790fb6376429e17767e76ae65abc3f19c17f58346d1f9a15
|
|
| MD5 |
6b69da2351cde0ef42327a33c5f3299c
|
|
| BLAKE2b-256 |
7c24a48c8605b477d56ffcc2979e510ea8c72fb2a1a62a62f4a799c1c24e1e07
|