Creating Plugins¶
Build tools in your own repository. No onetool source required.
Plugins let you develop OneTool tools in separate repositories while using local configuration for development and testing.
Minimal Structure¶
A plugin needs just one file:
The tool file¶
# src/mytool.py
pack = "mytool"
__all__ = ["search"]
def search(*, query: str) -> str:
"""Search for items.
Args:
query: The search query
Returns:
Search results
"""
return f"Found: {query}"
That's the minimum. One file with a pack declaration and exported functions.
Local Development Setup¶
For development, create a .onetool/ directory in your plugin repository:
ot-mytool/
├── .onetool/
│ ├── ot-serve.yaml # Server config (tools_dir, etc.)
│ ├── secrets.yaml # API keys for testing
│ └── ot-bench.yaml # Benchmark harness config (optional)
├── demo.yaml # Test scenarios (ot-bench run demo.yaml)
└── src/
└── mytool.py
.onetool/ot-serve.yaml¶
Point tools_dir at your plugin source:
Run ot-serve from your plugin directory. It finds .onetool/ot-serve.yaml automatically.
.onetool/secrets.yaml¶
Add API keys your tool needs during development:
.onetool/ot-bench.yaml¶
Configure the benchmark harness (model, evaluators, server definitions):
# .onetool/ot-bench.yaml
defaults:
timeout: 60
model: anthropic/claude-sonnet-4
servers:
mytool:
type: stdio
command: ot-serve
Test Scenario Files¶
Define test scenarios in a separate YAML file:
# demo.yaml
scenarios:
- name: "Basic search test"
tasks:
- name: "search:basic"
server: mytool
prompt: "Search for python tutorials using mytool.search"
Run tests with: ot-bench run demo.yaml
Running Locally¶
From your plugin directory:
The server discovers your tool from the local tools_dir configuration.
Worker Tools (with dependencies)¶
If your tool needs external packages, use PEP 723 headers and run as an isolated subprocess:
# /// script
# requires-python = ">=3.11"
# dependencies = ["ot-sdk>=0.1.0", "httpx>=0.28.0", "pyyaml>=6.0.0"]
# ///
"""Tool with external dependencies."""
from __future__ import annotations
pack = "mytool"
__all__ = ["fetch"]
from ot_sdk import http, log, worker_main
def fetch(*, url: str) -> str:
"""Fetch a URL.
Args:
url: URL to fetch
Returns:
Page content
"""
with log("mytool.fetch", url=url) as s:
response = http.get(url)
s.add(status=response.status_code)
return response.text
if __name__ == "__main__":
worker_main()
Critical: The if __name__ == "__main__": worker_main() block is required for any file with a PEP 723 header. Without it, the tool fails with "Worker closed unexpectedly".
SDK Exports¶
The ot_sdk package provides utilities for worker tools:
| Export | Purpose |
|---|---|
worker_main |
Main loop - handles JSON-RPC requests |
get_config(key) |
Access configuration from ot-serve.yaml |
get_secret(key) |
Access secrets from secrets.yaml |
http |
Pre-configured httpx client |
log(span, **kwargs) |
Structured logging context manager |
cache(ttl=seconds) |
In-memory caching decorator |
get_project_path(path) |
Resolve paths relative to project directory |
get_config_path(path) |
Resolve paths relative to config directory |
Consumer Installation¶
When users want to use your plugin, they add it to their tools_dir:
Global installation¶
Project-specific¶
Glob patterns work for selecting tool files.
Testing Without Full Installation¶
Test your plugin functions directly without running ot-serve:
# test_mytool.py
from mytool import search
def test_search():
result = search(query="python")
assert "python" in result.lower()
Run with pytest:
For worker tools, test the functions before adding worker_main():
# Test individual functions
from mytool import fetch
def test_fetch():
# Mock http if needed
result = fetch(url="https://example.com")
assert result
Example: Plugin with Implementation Modules¶
For larger plugins, organize implementation in a subpackage:
ot-convert/
├── .onetool/
│ ├── ot-serve.yaml
│ └── secrets.yaml
├── src/
│ ├── convert.py # Main tool file (worker)
│ └── _convert/ # Implementation modules
│ ├── __init__.py
│ ├── pdf.py
│ └── word.py
└── README.md
The main tool file imports from the implementation package:
# /// script
# requires-python = ">=3.11"
# dependencies = ["ot-sdk>=0.1.0", "pymupdf>=1.24.0", "python-docx>=1.1.0"]
# ///
"""Document conversion tools."""
from __future__ import annotations
pack = "convert"
__all__ = ["pdf", "word"]
from ot_sdk import get_project_path, log, worker_main
from _convert import convert_pdf, convert_word
def pdf(*, pattern: str, output_dir: str = "output") -> str:
"""Convert PDF files to markdown."""
with log("convert.pdf", pattern=pattern) as s:
return convert_pdf(pattern, output_dir)
def word(*, pattern: str, output_dir: str = "output") -> str:
"""Convert Word documents to markdown."""
with log("convert.word", pattern=pattern) as s:
return convert_word(pattern, output_dir)
if __name__ == "__main__":
worker_main()