Spaces:
Runtime error
ToolStore Refactor Plan β Toolset (Agent-Centric Managed Tools)
1. Overview & Motivation
Current State
| Type | Mechanism | Managed By |
|---|---|---|
api |
JSON definition β HTTP call via httpx | Us |
mcp |
Discovered from external MCP servers | Client |
skill |
SKILL.md (YAML + Markdown), progressive disc. | Client |
docker |
Base64-encoded Python β warm Docker container | Us |
Problem: api and docker are ad-hoc. An API "tool" is just a URL + method β no custom logic, no code reuse. A docker "tool" is a raw base64 blob β no structure, no metadata.
Skills are human-centric (progressive disclosure: load β browse files β read file β run script). Agents need to do a multi-step dance just to use one. That friction is appropriate for human-authored instructional content, not for executable code.
The new toolset is agent-centric: the agent calls tool_store(action="execute", tool_name="weather", ...) and it just runs.
Goal
- Keep
mcp(external MCP servers β client-managed) andskill(human-centric progressive disclosure β client-managed). - Introduce
toolsetβ a new type we manage. Agent-centric. 1 doc file + 1 code file. Code has@tooldecorator bindings. - Remove
apianddocker. Their functionality is refactored into toolsets.
2. New Type: toolset
2.1 The Core Idea
A toolset is a directory with exactly two files:
weather/ # directory name = toolset name
βββ doc.md # agent-facing documentation
βββ toolset.py # code with @tool-decorated functions
That's it. No scripts/, no references/, no assets/, no progressive disclosure, no SKILL.md. The agent calls it and it runs.
2.2 doc.md β Agent-Facing Documentation
Plain markdown. Describes what the toolset does, what functions are available, what parameters they take, what they return. The agent reads this to understand the tool.
# Weather Toolset
Get weather data from public APIs.
## Functions
### get_weather
Get current weather for a location.
| Parameter | Type | Required | Default | Description |
|-----------|--------|----------|---------|--------------------|
| location | string | yes | β | City name or coords|
| units | string | no | metric | metric / imperial |
Returns: JSON with temperature, humidity, wind speed.
### get_forecast
Get 5-day forecast.
| Parameter | Type | Required | Default | Description |
|-----------|---------|----------|---------|--------------|
| location | string | yes | β | City name |
| days | integer | no | 5 | Number of days|
Returns: JSON array of daily forecasts.
2.3 toolset.py β Code with @tool Bindings
The code file uses a @tool decorator to mark functions as callable entry points:
from toolstore.toolset import tool
@tool
def get_weather(location: str, units: str = "metric"):
"""Get current weather for a location."""
import httpx
resp = httpx.get(
"https://api.openweathermap.org/data/2.5/weather",
params={"q": location, "units": units, "appid": "..."}
)
resp.raise_for_status()
return resp.json()
@tool
def get_forecast(location: str, days: int = 5):
"""Get 5-day forecast for a location."""
import httpx
resp = httpx.get(
"https://api.openweathermap.org/data/2.5/forecast",
params={"q": location, "cnt": days * 8}
)
resp.raise_for_status()
return resp.json()
@tool decorator rules:
- Marks a function as an entry point callable by agents.
- The function name becomes the binding name.
- The function docstring is used as the binding description.
- Function signature (params + type hints + defaults) defines the input schema.
- Return value is JSON-serialized and sent back to the agent.
Only @tool-decorated functions are callable. Other functions/classes in toolset.py are private helpers (not exposed).
2.4 How The Agent Uses It
One call, no ceremony:
tool_store(
action="execute",
tool_name="weather",
arguments={"function": "get_weather", "location": "London", "units": "metric"}
)
That's it. No load/files/file/run dance. The agent can also request the doc:
tool_store(action="info", tool_name="weather")
Which returns the toolset definition including the full doc.md body.
2.5 How It Subsumes api and docker
| Old type | New equivalent |
|---|---|
api β {"endpoint": "...", "method": "GET", "parameters": {...}} |
A toolset.py with @tool functions that use httpx. Full Python β headers, auth, retries, response parsing, chaining calls β not just a rigid URL + method template. |
docker β {"code": "<base64>", "function": "main"} |
A toolset.py with @tool functions. Same warm Docker worker executes it. But now it has proper structure, metadata, and a doc.md. |
3. toolset Module (the @tool decorator)
A lightweight module (toolstore/toolset.py) that toolsets import:
# toolstore/toolset.py
"""
Decorator module imported by toolset code files.
Provides the @tool decorator that marks functions as agent-callable.
"""
from typing import Callable, Any
_REGISTRY: dict[str, Callable] = {}
def tool(fn: Callable) -> Callable:
"""Decorator: mark a function as a toolset entry point."""
_REGISTRY[fn.__name__] = fn
fn._is_toolset_tool = True
return fn
def get_tool(name: str) -> Callable | None:
"""Get a registered tool function by name."""
return _REGISTRY.get(name)
def get_tool_names() -> list[str]:
"""Get all registered tool names."""
return list(_REGISTRY.keys())
def clear_registry() -> None:
"""Clear the registry (called between toolset loads)."""
_REGISTRY.clear()
This module lives inside ToolStore itself (not a third-party dependency). Toolsets import it with from toolstore.toolset import tool.
4. Discovery & Registration
4.1 Toolset Discovery
A new ToolsetManager scans configured directories for toolset.py files:
toolsets/ # configured toolset directory
βββ weather/toolset.py β name="weather", type="toolset"
βββ github/toolset.py β name="github", type="toolset"
βββ calculator/toolset.py β name="calculator", type="toolset"
Each directory containing a toolset.py is a toolset. The directory name becomes the tool name.
4.2 Registration
On scan, ToolsetManager:
- Reads
doc.mdfrom the toolset directory. - Imports
toolset.pyin a controlled way (parses@tool-decorated functions without executing them). - Extracts function names, signatures, docstrings, and parameter info.
- Builds a tool definition with a proper JSON Schema input.
4.3 Tool Definition (index entry)
{
"name": "weather",
"type": "toolset",
"description": "Get weather data from public APIs.",
"source": "toolset",
"toolset_dir": "/path/to/toolsets/weather",
"doc": "<raw doc.md content>",
"bindings": {
"get_weather": {
"description": "Get current weather for a location.",
"parameters": {
"location": {"type": "string", "required": true},
"units": {"type": "string", "required": false, "default": "metric"}
}
},
"get_forecast": {
"description": "Get 5-day forecast for a location.",
"parameters": {
"location": {"type": "string", "required": true},
"days": {"type": "integer", "required": false, "default": 5}
}
}
},
"schema": {
"input_schema": {
"type": "object",
"properties": {
"function": {
"type": "string",
"enum": ["get_weather", "get_forecast"],
"description": "Which function to call in this toolset"
},
"location": {"type": "string", "description": "..."},
"units": {"type": "string", "description": "..."},
"days": {"type": "integer", "description": "..."}
}
}
}
}
5. Execution Model
5.1 Flow
tool_store_tool(action="execute", tool_name="weather",
function="get_weather", location="London", units="metric")
β
ββ index_manager.get_tool("weather") β type="toolset"
β
ββ _execute_toolset(tool, args)
β
ββ 1. Read toolset.py from disk
ββ 2. Extract function "get_weather" from args
ββ 3. Load module in warm Docker worker
β docker_pool.get_worker().load_module(name, code)
ββ 4. Call the function
β worker.call_function(name, "get_weather", fn_args, timeout)
ββ 5. Return JSON result
5.2 Sandbox (reuses docker_pool.py)
Same warm Docker worker as today's docker tools. The worker:
- Loads the Python module (with
@tooldecorator available in its environment) - Calls
get_tool(name)to get the function - Calls the function with the agent's arguments
- Serializes the return value as JSON
5.3 Argument Passing
The function argument selects which @tool function to call. All other arguments are passed as kwargs to the function:
agent calls: arguments={"function": "get_weather", "location": "London", "units": "metric"}
becomes: get_weather(location="London", units="metric")
If function is omitted and there's only one @tool function, it's called directly.
6. What Gets Removed
6.1 Client (toolstore/)
| File | Change |
|---|---|
native_tool.py |
Remove _execute_api() (L200-227), _execute_docker() (L308-357), and their branches in _do_execute(). Add _execute_toolset(). |
mcp_server.py |
Remove _execute_api() (L176-197), add _execute_toolset(). Remove api/docker from dispatch. |
cli.py |
Remove publish-api, publish-docker commands. Add publish-toolset command. Remove api/docker interactive sections. |
schema_converter.py |
No changes needed (it already handles arbitrary tool defs by reading schema). |
index_manager.py |
No changes. Type-agnostic. |
config_manager.py |
Add get_toolset_dirs() alongside existing get_skill_dirs(). |
docker_pool.py |
Update docstrings: "executes toolsets" instead of "docker tools". Add @tool decorator stub to the warm worker environment. |
transport.py |
No changes (transport is MCP-only). |
management/server.py |
Remove api/docker UI and endpoints. Add toolset management. |
skill_manager.py |
No changes. Skills stay as-is. |
6.2 Server (server/app/)
| File | Change |
|---|---|
models.py |
Remove APITool and DockerTool models. Add ToolsetTool model. |
main.py |
Remove /tools/api and /tools/docker endpoints. Add /tools/toolset endpoint. |
6.3 New Files
| File | Purpose |
|---|---|
toolstore/toolset.py |
The @tool decorator module that toolsets import |
toolstore/toolset_manager.py |
Discovery, parsing, registration (reads doc.md + inspects toolset.py) |
7. Final Type Taxonomy
| Type | Managed By | Description |
|---|---|---|
mcp |
Client | External MCP servers (unchanged) |
skill |
Client | Human-centric SKILL.md progressive disclosure (unchanged) |
toolset |
Us | Agent-centric: 1 doc + 1 code, @tool bindings, just runs |
All future "managed by us" extensibility goes through toolset. No other custom types.
8. Key Design Decisions
@toolis a Python decorator, not a markdown annotation. It lives in the code file, where code belongs. Simple, standard Python.- 1 doc + 1 code. Exactly two files. No subdirectories, no scripts/, no assets/, no SKILL.md. Frictionless for agents.
- Agents just execute. No progressive disclosure, no load/files/file/run. One call:
execute. - Skills stay human-centric. Toolsets are the agent-centric sibling, not a replacement.
- Same warm Docker worker.
docker_pool.pyis repurposed, not rewritten. - The
@tooldecorator IS the binding. Function name = binding name. Docstring = description. Signature = schema. No duplication.
9. Example: Converting API β Toolset
Before (api JSON)
{
"name": "weather-api",
"type": "api",
"endpoint": "https://api.weather.com/data",
"method": "GET",
"parameters": {
"location": {"type": "string", "required": true},
"units": {"type": "string", "required": false, "default": "metric"}
}
}
After (toolset)
weather/doc.md:
# Weather Toolset
### get_weather
Get weather for a location.
- `location` (string): City name
- `units` (string, default "metric"): metric or imperial
weather/toolset.py:
from toolstore.toolset import tool
@tool
def get_weather(location: str, units: str = "metric"):
"""Get current weather for a location."""
import httpx
resp = httpx.get(
"https://api.weather.com/data",
params={"location": location, "units": units}
)
resp.raise_for_status()
return resp.json()
Agent calls it:
tool_store(action="execute", tool_name="weather",
arguments={"function": "get_weather", "location": "London"})
Before (docker JSON)
{
"name": "calculator",
"type": "docker",
"code": "def main(a, b, op): ...",
"function": "main"
}
After (toolset)
calculator/doc.md:
# Calculator Toolset
### compute
Basic arithmetic.
- `a` (number): First operand
- `b` (number): Second operand
- `op` (string): add, subtract, multiply, divide
calculator/toolset.py:
from toolstore.toolset import tool
@tool
def compute(a: float, b: float, op: str = "add"):
"""Basic arithmetic."""
ops = {"add": a + b, "subtract": a - b,
"multiply": a * b, "divide": a / b}
return {"result": ops[op]}
10. Migration Path
Phase 1: Add toolset (zero breaking changes)
- Create
toolstore/toolset.py(the@tooldecorator). - Create
toolstore/toolset_manager.py. - Add
_execute_toolset()innative_tool.py. - Add toolset dispatch in
mcp_server.py. - Add toolset to
config_manager.py. - Add server-side
ToolsetToolmodel + endpoint. - Add
publish-toolsetCLI command.
All four types coexist. No existing tool breaks.
Phase 2: Convert existing tools
- Migration script: api JSON β toolset directory.
- Migration script: docker JSON β toolset directory.
- Convert all examples and reference tools.
Phase 3: Remove api and docker
- Delete
_execute_api()+_execute_docker()from native_tool.py and mcp_server.py. - Remove
APITool+DockerToolfrom server models. - Remove api/docker CLI commands, endpoints, UI sections.
- Clean up stale references.
Phase 4: Polish
- Per-toolset timeout and env var config.
- Toolset validation CLI (
toolstore validate-toolset <path>). - Support for Node.js toolsets (
runtime: nodeβ warm Node container). - Package dependency declarations.