Tool Module
Tools live in tools/<name>/ and mount at /tools/{key}. The framework handles routing, admin config UI, tags, and visibility — the module only needs a Register func.
Looking for LLM-facing modules?
Tools are designed for humans clicking a UI. For modules consumed by LLM clients (Claude, Cursor) over MCP, see Connector Module.
Example tool — Convert Text. Left panel lists modes, right panel handles input/output.
Tool settings — runtime config values editable without redeploying.
File Structure
tools/my-tool/
├── handler.go # Register func + HTTP handler funcs
├── service.go # business logic (pure Go)
├── repo.go # external I/O — DB, HTTP, S3 (stub if not needed)
├── view.templ # templ HTML template
├── config.go # typed Config struct (if tool has runtime knobs)
├── static.go # //go:embed declaration (if JS assets)
└── js/
└── mytool.js # tool-scoped JS, no CDNRegister in main.go
app.RegisterTool(
tool.Tool{
Key: "my-tool",
Name: "My Tool",
Description: "What this tool does.",
Icon: "🔧",
Category: "Text",
DefaultVisibility: entity.VisibilityPublic,
DefaultTags: []tool.DefaultTag{tags.Text},
},
mytool.Config{InitText: "hello"},
mytool.Register,
)One call = one card on the home grid. Call again with a different Key (and optionally a different Config) to get a second card backed by the same Register func.
For tools with no runtime config:
app.RegisterToolNoConfig(
tool.Tool{Key: "dashboard", Name: "Dashboard", Icon: "📊", ExternalURL: "https://grafana.example.com"},
external.Register,
)tool.Tool fields
| Field | Description |
|---|---|
Key | Unique slug, kebab-case. Drives the mount path /tools/{Key} |
Name | Display name shown on the card and page title |
Description | Card subtitle |
Icon | Emoji or short string shown on the card |
Category | Groups cards visually on the home grid |
DefaultVisibility | entity.VisibilityPublic or entity.VisibilityPrivate |
DefaultTags | Slice of tool.DefaultTag from tags/defaults.go |
ExternalURL | If set, card opens this URL in a new tab |
Register Function
package mytool
import "github.com/yogasw/wick/pkg/tool"
func Register(r tool.Router) {
r.GET("/", index)
r.POST("/", submit)
r.Static("/static/", StaticFS) // only if you have JS assets
}All paths are relative to /tools/{key} — never hardcode the full path.
Mounting a sub-router or reverse proxy
When a tool wraps an external handler that owns its own sub-routing (WebSocket proxy, embedded HTTP server), use r.HandleRaw:
r.HandleRaw("/tty/", func(cfg tool.ConfigReader) http.Handler {
inner := externalSrv.Handler()
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if cfg.GetOwned("mytool", "enabled") != "true" {
http.Error(w, "disabled", http.StatusForbidden)
return
}
inner.ServeHTTP(w, req)
})
})prefixis relative to/tools/{key}and must end with/fnreceives atool.ConfigReader— usecfg.GetOwned(key, field)to gate on runtime config- Use sparingly — prefer
r.GET/r.POSTfor normal endpoints
Handlers
Handlers are plain top-level funcs that receive *tool.Ctx:
func index(c *tool.Ctx) {
seed := c.Cfg("init_text")
c.HTML(IndexBody(c.Meta().Name, c.Base(), seed))
}
func submit(c *tool.Ctx) {
input := c.Form("input")
c.HTML(IndexBody(c.Meta().Name, c.Base(), process(input)))
}Ctx helpers
| Helper | Description |
|---|---|
c.Base() | Absolute base path /tools/{key} |
c.Meta() | The registered tool.Tool (Key, Name, Icon, …) |
c.Cfg(key) | Read runtime config value for this instance |
c.CfgInt(key) | Config value as int |
c.CfgBool(key) | Config value as bool |
c.Missing() | required config keys not yet set |
c.Form(key) | Form field value |
c.Query(key) | Query string value |
c.BindJSON(&v) | Decode JSON body |
c.HTML(body) | Write HTML response |
c.JSON(status, v) | Write JSON response |
c.Redirect(url, code) | Redirect |
Runtime Config
Declare a Config struct in config.go:
package mytool
type Config struct {
InitText string `wick:"desc=Seed text on first load."`
APIKey string `wick:"desc=External API key.;secret;required"`
MaxItems int `wick:"desc=Max results.;number"`
Mode string `wick:"desc=Processing mode.;dropdown=fast|accurate|balanced"`
}The framework reflects the struct into configs table rows at boot. Admin edits are live on the next request — no redeploy.
For the full widget table, all tag flags, key derivation rules, and the kvlist editable-table type, see the Config Tag Reference.
JavaScript Assets
// static.go
package mytool
import "embed"
//go:embed js
var StaticFS embed.FSMount and reference in handler + templ:
r.Static("/static/", StaticFS)<script src={ base + "/static/js/mytool.js" }></script>WARNING
//go:embed js fails if js/ doesn't exist. Create the directory with at least one file before running go build.
Tags
Add shared tags in tags/defaults.go:
var MyGroup = tool.DefaultTag{
Name: "MyGroup",
Description: "Tools for X.",
IsGroup: true,
SortOrder: 20,
}Reference in main.go:
DefaultTags: []tool.DefaultTag{tags.MyGroup},TIP
Check if an existing tag fits before adding a new one — fewer tags keeps the home grid clean.