Skip to content

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.

Tool DetailExample tool — Convert Text. Left panel lists modes, right panel handles input/output.

Tool SettingsTool 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 CDN

Register in main.go

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:

go
app.RegisterToolNoConfig(
    tool.Tool{Key: "dashboard", Name: "Dashboard", Icon: "📊", ExternalURL: "https://grafana.example.com"},
    external.Register,
)

tool.Tool fields

FieldDescription
KeyUnique slug, kebab-case. Drives the mount path /tools/{Key}
NameDisplay name shown on the card and page title
DescriptionCard subtitle
IconEmoji or short string shown on the card
CategoryGroups cards visually on the home grid
DefaultVisibilityentity.VisibilityPublic or entity.VisibilityPrivate
DefaultTagsSlice of tool.DefaultTag from tags/defaults.go
ExternalURLIf set, card opens this URL in a new tab

Register Function

go
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:

go
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)
    })
})
  • prefix is relative to /tools/{key} and must end with /
  • fn receives a tool.ConfigReader — use cfg.GetOwned(key, field) to gate on runtime config
  • Use sparingly — prefer r.GET/r.POST for normal endpoints

Handlers

Handlers are plain top-level funcs that receive *tool.Ctx:

go
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

HelperDescription
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:

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

go
// static.go
package mytool

import "embed"

//go:embed js
var StaticFS embed.FS

Mount and reference in handler + templ:

go
r.Static("/static/", StaticFS)
html
<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:

go
var MyGroup = tool.DefaultTag{
    Name:        "MyGroup",
    Description: "Tools for X.",
    IsGroup:     true,
    SortOrder:   20,
}

Reference in main.go:

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.

Built with ❤️ by a developer, for developers.