Skip to content

Commit 390d550

Browse files
SonAIengineclaude
andcommitted
docs: README 전면 재작성 — 뱃지, 실제 출력 예시, 구조 다이어그램, 간결한 rule 테이블
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b1878c4 commit 390d550

1 file changed

Lines changed: 131 additions & 137 deletions

File tree

README.md

Lines changed: 131 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,215 +1,209 @@
11
# Toolint
22

3-
**Structural linter for MCP-compatible, zero-dependency Python agent tool packages.**
3+
[![CI](https://github.com/PlateerLab/Toolint/actions/workflows/ci.yml/badge.svg)](https://github.com/PlateerLab/Toolint/actions/workflows/ci.yml)
4+
[![PyPI](https://img.shields.io/pypi/v/toolint)](https://pypi.org/project/toolint/)
5+
[![Python](https://img.shields.io/pypi/pyversions/toolint)](https://pypi.org/project/toolint/)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
47

5-
`toolint` enforces architectural rules that ensure your Python package works correctly as:
6-
- A **standalone library** (`from my_tool import MyTool`)
7-
- A **CLI tool** (`my-tool search "query"`)
8-
- An **MCP server** (`my-tool serve --source spec.json`)
9-
- An **SDK middleware** (OpenAI / Anthropic client patches)
8+
**Structural linter for Python agent tool packages.**
109

11-
Inspired by the architecture of [graph-tool-call](https://github.com/SonAIengine/graph-tool-call).
10+
Ensures your package works as a **library**, **CLI**, and **MCP server** simultaneously — with zero-dependency core and proper facade separation.
1211

13-
## Why?
12+
## The Problem
1413

15-
Building agent-compatible tools is easy to get wrong:
14+
AI agent tools need to work in multiple contexts at once:
1615

17-
| Mistake | Consequence |
18-
|---------|------------|
19-
| `core/` imports `numpy` without guard | Users without numpy get `ImportError` on `import my_tool` |
20-
| MCP server has business logic | Can't use the same functionality as a library |
21-
| `__version__` != `pyproject.toml` version | PyPI shows wrong version |
22-
| Tool function has no docstring | LLM can't understand what the tool does |
23-
| Optional dep not in `extras` | `pip install my-tool[mcp]` doesn't install MCP SDK |
16+
```python
17+
# As a library
18+
from my_tool import MyTool
19+
tg = MyTool()
20+
tg.search("query")
2421

25-
`toolint` catches all of these **before** they reach users.
22+
# As a CLI
23+
$ my-tool search "query"
24+
25+
# As an MCP server
26+
$ my-tool serve --source spec.json
27+
```
28+
29+
Getting this right requires strict architectural discipline. Without it:
30+
31+
- `core/` imports `numpy` → users get `ImportError` just from `import my_tool`
32+
- MCP server has business logic → can't reuse the same functionality as a library
33+
- CLI calls internal modules directly → refactoring breaks everything
34+
- Tool function has no docstring → LLM can't select the right tool
35+
- `__version__` doesn't match `pyproject.toml` → PyPI shows wrong version
36+
37+
**Toolint catches all of these statically, before they reach users.**
2638

2739
## Installation
2840

2941
```bash
3042
pip install toolint
3143

32-
# or with uv
33-
uv pip install toolint
34-
35-
# or as a tool
44+
# or run without installing
3645
uvx toolint check .
3746
```
3847

39-
## Quick Start
48+
## Usage
4049

4150
```bash
42-
# Lint current project
51+
# Lint a project
4352
toolint check .
4453

45-
# Lint with specific rules only
46-
toolint check . --select ATL101,ATL102
54+
# Select specific rules
55+
toolint check . --select ATL101,ATL201
4756

48-
# Ignore specific rules
49-
toolint check . --ignore ATL105
57+
# Ignore rules
58+
toolint check . --ignore ATL105,ATL501
5059

51-
# JSON output (for CI integration)
60+
# JSON output for CI
5261
toolint check . --format json
5362

54-
# Show all available rules
63+
# List all rules
5564
toolint rules
5665
```
5766

58-
## Example Output
67+
## Real-World Example
68+
69+
Running `toolint` against [graph-tool-call](https://github.com/SonAIengine/graph-tool-call) (248-tool search engine, 1K+ stars worth of architecture):
5970

6071
```
61-
my_tool/core/engine.py:3:0 ATL101 (error)
62-
Hard import of 'numpy' in core module — core must be stdlib-only.
63-
Use try/except ImportError guard or move to a non-core module.
72+
graph_tool_call/core/graph.py:9:4 ATL101 (error)
73+
Third-party import 'networkx' in core module — core/ must be stdlib-only.
74+
Move this module outside of core/, or add 'networkx' to
75+
core_allowed_imports in [tool.toolint].
76+
77+
graph_tool_call/mcp_server.py:128:0 ATL201 (warning)
78+
'mcp_server.py' imports internal module 'graph_tool_call.retrieval.engine'
79+
instead of using facade 'ToolGraph'.
6480
65-
my_tool/retrieval/embedding.py:5:0 ATL102 (error)
66-
Optional import 'sentence_transformers' missing try/except ImportError guard.
81+
graph_tool_call/tool_graph.py:410:0 ATL501 (warning)
82+
Facade method 'ToolGraph.add_domain()' has no docstring.
83+
84+
11 issues found (1 error, 10 warnings)
85+
```
6786

68-
my_tool/__init__.py:1:0 ATL004 (error)
69-
Version mismatch: __init__.py has '0.2.0' but pyproject.toml has '0.2.1'
87+
## Architecture Enforced
7088

71-
my_tool/mcp_server.py:42:8 ATL503 (error)
72-
MCP tool function 'process_data' has no docstring.
73-
LLMs rely on tool descriptions to select the right tool.
89+
Toolint validates this package structure:
7490

75-
4 issues found (4 errors, 0 warnings)
91+
```
92+
my_package/
93+
├── __init__.py # __version__, __all__, lazy imports
94+
├── __main__.py # CLI — calls facade only
95+
├── core/ # stdlib ONLY — no external deps
96+
│ ├── protocol.py # Abstract interfaces (Protocol)
97+
│ └── models.py # Domain models (dataclass)
98+
├── feature_a/ # Business logic (optional deps with guards)
99+
├── facade.py # Single public API class
100+
├── mcp_server.py # MCP server — wraps facade
101+
└── middleware.py # SDK patches — wraps facade
76102
```
77103

78-
## Rules
104+
Four principles:
79105

80-
### Layer 1: Structure (ATL0xx)
106+
1. **Core is stdlib-only**`import my_tool` always works, no extras needed
107+
2. **Facade is the single API** — CLI, MCP, middleware all go through one class
108+
3. **Optional deps use import guards** — graceful degradation, not crashes
109+
4. **Interface layers are thin** — no business logic in MCP server or CLI
81110

82-
| Rule | Severity | Description |
83-
|------|----------|-------------|
84-
| `ATL001` | error | Package must have a single public facade class |
85-
| `ATL002` | error | `__main__.py` must exist and be registered in pyproject.toml scripts |
86-
| `ATL003` | warning | `__init__.py` must define `__all__` including the facade class |
87-
| `ATL004` | error | `__version__` in `__init__.py` must match pyproject.toml version |
111+
## Rules
88112

89-
### Layer 2: Dependency Rules (ATL1xx)
113+
### Structure (ATL0xx)
90114

91-
| Rule | Severity | Description |
92-
|------|----------|-------------|
93-
| `ATL101` | error | No third-party imports in `core/` directory (stdlib only) |
94-
| `ATL102` | error | Optional dependencies must use `try/except ImportError` guard |
95-
| `ATL103` | warning | Import guard must include install hint (e.g. `pip install pkg[extra]`) |
96-
| `ATL104` | error | Optional imports must be registered in pyproject.toml `extras` |
97-
| `ATL105` | warning | `__init__.py` should not eagerly import optional-dep modules |
115+
| Rule | Sev | What it checks |
116+
|------|-----|----------------|
117+
| `ATL001` | error | Facade class exists in the package |
118+
| `ATL002` | error | `__main__.py` exists |
119+
| `ATL003` | warn | `__init__.py` has `__all__` with facade class |
120+
| `ATL004` | error | `__version__` matches `pyproject.toml` |
98121

99-
### Layer 3: Layer Separation (ATL2xx)
122+
### Dependencies (ATL1xx)
100123

101-
| Rule | Severity | Description |
102-
|------|----------|-------------|
103-
| `ATL201` | warning | Interface files should not call internal business logic directly (type/enum/constant imports are allowed) |
104-
| `ATL202` | warning | CLI command handlers should invoke functionality through the facade class |
105-
| `ATL203` | warning | Interface layer should not import `core/` internals directly (except types/constants) |
124+
| Rule | Sev | What it checks |
125+
|------|-----|----------------|
126+
| `ATL101` | error | `core/` has no third-party imports |
127+
| `ATL102` | error | Optional deps use `try/except ImportError` |
128+
| `ATL103` | warn | Import guard has install hint message |
129+
| `ATL104` | error | Guarded imports are in `pyproject.toml` extras |
130+
| `ATL105` | warn | `__init__.py` doesn't eagerly import optional deps |
106131

107-
### Layer 4: pyproject.toml Consistency (ATL3xx)
132+
### Layer Separation (ATL2xx)
108133

109-
| Rule | Severity | Description |
110-
|------|----------|-------------|
111-
| `ATL301` | error | CLI entry point must be registered in `[tool.poetry.scripts]` or `[project.scripts]` |
112-
| `ATL302` | error | If MCP server exists, `mcp` extras group must be defined |
113-
| `ATL303` | warning | `all` extras group must include all dependencies from other extras groups |
134+
| Rule | Sev | What it checks |
135+
|------|-----|----------------|
136+
| `ATL201` | warn | Interface files go through facade, not internal modules |
137+
| `ATL202` | warn | CLI references the facade class |
138+
| `ATL203` | warn | Interface doesn't import `core/` directly (types allowed) |
114139

115-
### Layer 5: Tool Schema Quality (ATL5xx)
140+
### pyproject.toml (ATL3xx)
116141

117-
| Rule | Severity | Description |
118-
|------|----------|-------------|
119-
| `ATL501` | warning | Facade public methods must have docstrings |
120-
| `ATL502` | warning | Facade public methods must have parameter + return type hints |
121-
| `ATL503` | error | MCP tool functions must have docstrings (min 10 chars) |
122-
| `ATL504` | warning | MCP tool function docstrings should describe each parameter |
142+
| Rule | Sev | What it checks |
143+
|------|-----|----------------|
144+
| `ATL301` | error | CLI scripts entry registered |
145+
| `ATL302` | error | MCP server present → `mcp` extras defined |
146+
| `ATL303` | warn | `all` extras includes everything |
123147

124-
### Layer 6: Agent Compatibility (ATL6xx)
148+
### Schema Quality (ATL5xx)
125149

126-
| Rule | Severity | Description |
127-
|------|----------|-------------|
128-
| `ATL601` | warning | Facade public methods should return JSON-serializable types |
129-
| `ATL602` | error | MCP tool functions must return `str` (MCP protocol requirement) |
130-
| `ATL603` | warning | Facade/MCP tools should not silently swallow exceptions |
150+
| Rule | Sev | What it checks |
151+
|------|-----|----------------|
152+
| `ATL501` | warn | Facade public methods have docstrings |
153+
| `ATL502` | warn | Facade public methods have type hints |
154+
| `ATL503` | error | MCP tool functions have docstrings (min 10 chars) |
155+
| `ATL504` | warn | MCP tool docstrings describe parameters |
131156

132157
## Configuration
133158

134-
Add to `pyproject.toml`:
135-
136159
```toml
160+
# pyproject.toml
137161
[tool.toolint]
138-
# Package root (auto-detected from pyproject.toml)
139-
package = "my_tool"
140-
141-
# Facade class name (auto-detected if single prominent class exists)
142-
facade_class = "MyTool"
143-
144-
# Core directory (default: "core")
145-
core_dir = "core"
146-
147-
# Interface files (default: auto-detected)
148-
interface_files = ["mcp_server.py", "mcp_proxy.py", "middleware.py", "__main__.py"]
149-
150-
# Extra stdlib-like packages allowed in core (escape hatch)
151-
core_allowed_imports = []
152-
153-
# Rules to ignore
154-
ignore = ["ATL105"]
162+
package = "my_tool" # auto-detected
163+
facade_class = "MyTool" # auto-detected
164+
core_dir = "core" # default
165+
interface_files = [ # default
166+
"mcp_server.py", "mcp_proxy.py",
167+
"middleware.py", "__main__.py"
168+
]
169+
core_allowed_imports = [] # escape hatch for core/
170+
ignore = ["ATL105"] # rules to skip
155171
```
156172

157-
Or use a standalone file `.toolint.toml` with the same structure (without the `[tool.toolint]` nesting).
158-
159-
## The Architecture This Enforces
160-
161-
```
162-
my_package/
163-
├── __init__.py # __version__, __all__, lazy imports
164-
├── __main__.py # CLI (argparse) — calls facade only
165-
├── core/ # ZERO external dependencies (stdlib only)
166-
│ ├── protocol.py # Abstract interfaces (Protocol classes)
167-
│ └── models.py # Domain models (dataclasses)
168-
├── feature_a/ # Business logic modules
169-
│ └── ... # May use optional deps with import guards
170-
├── facade.py # Single public API class
171-
├── mcp_server.py # MCP server — wraps facade only
172-
└── middleware.py # SDK patches — wraps facade only
173-
```
174-
175-
**Key principles:**
176-
1. **Core is stdlib-only** — anyone can `import my_tool` without installing extras
177-
2. **Facade is the single API surface** — all interfaces (CLI, MCP, middleware) go through it
178-
3. **Optional deps use import guards** — graceful degradation, not crashes
179-
4. **Interface layers are thin wrappers** — no business logic in MCP server or CLI
173+
Or use `.toolint.toml` as a standalone config file.
180174

181175
## CI Integration
182176

183177
### GitHub Actions
184178

185179
```yaml
186-
- name: Lint agent tool structure
180+
- name: Structural lint
187181
run: uvx toolint check .
188182
```
189183
190184
### Pre-commit
191185
192186
```yaml
193187
repos:
194-
- repo: https://github.com/PlateerLab/toolint
188+
- repo: https://github.com/PlateerLab/Toolint
195189
rev: v0.1.0
196190
hooks:
197191
- id: toolint
198192
```
199193
200-
## Technical Details
194+
## How It Works
201195
202-
- **Python 3.10+**
203-
- **Zero dependencies** — uses only `ast` and `tomllib` from stdlib
204-
- **Fast** — AST parsing, no runtime imports of the target package
205-
- **Self-validating** — `toolint` follows the same architecture it enforces
196+
- **Zero dependencies** — stdlib only (`ast`, `tomllib`, `pathlib`)
197+
- **AST-based** — parses Python files without importing them
198+
- **Fast** — 66 tests run in 0.1s, real projects lint in milliseconds
199+
- **Python 3.10+** — uses `sys.stdlib_module_names` for accurate stdlib detection
206200

207-
## License
201+
## Links
208202

209-
MIT
203+
- [PyPI](https://pypi.org/project/toolint/)
204+
- [GitHub](https://github.com/PlateerLab/Toolint)
205+
- [graph-tool-call](https://github.com/SonAIengine/graph-tool-call) — reference architecture this linter validates
210206

211-
## Links
207+
## License
212208

213-
- [GitHub](https://github.com/PlateerLab/toolint)
214-
- [PyPI](https://pypi.org/project/toolint/) (coming soon)
215-
- [graph-tool-call](https://github.com/SonAIengine/graph-tool-call) — the reference implementation this linter is based on
209+
MIT

0 commit comments

Comments
 (0)